<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Matt Blewitt</title><link>https://matt.blwt.io/post/</link><description>Irregular Expressions</description><generator>Hugo</generator><language>en-gb</language><lastBuildDate>Sun, 24 Aug 2025 15:20:17 +0100</lastBuildDate><atom:link href="https://matt.blwt.io/post/index.xml" rel="self" type="application/rss+xml"/><item><title>Over-Engineering Sleep</title><link>https://matt.blwt.io/post/overengineering-sleep/</link><pubDate>Sun, 24 Aug 2025 15:20:17 +0100</pubDate><guid>https://matt.blwt.io/post/overengineering-sleep/</guid><description>&lt;p&gt;A while back, &lt;a href="https://github.com/actions/runner/pull/1707"&gt;this implementation&lt;/a&gt; of a cross-platform sleep for GitHub Actions Runners caught my eye, and I wanted to see what it would take to re-implement &lt;code&gt;sleep&lt;/code&gt; from the ground up, while trying to be as cross-platform as possible.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>A while back, <a href="https://github.com/actions/runner/pull/1707">this implementation</a> of a cross-platform sleep for GitHub Actions Runners caught my eye, and I wanted to see what it would take to re-implement <code>sleep</code> from the ground up, while trying to be as cross-platform as possible.</p>
<figure><img src="/overengineering-sleep/header.webp"
    alt="A sleeping computer monitor.">
</figure>

<h2 id="analysis">Analysis</h2>
<p>The motivation of this implementation seems to be around not relying on the <code>sleep</code> utility, <a href="https://pubs.opengroup.org/onlinepubs/9799919799/">despite this being part of the POSIX standard</a>. Instead, it uses a pure <code>bash</code> solution with the <code>SECONDS</code> <code>bash</code> builtin (<a href="https://man7.org/linux/man-pages/man1/bash.1.html">manpage reference</a>).</p>
<p>Looking at the source of <code>bash</code> reveals that this builtin is implemented using the <code>gettimeofday</code> call (<a href="https://github.com/bminor/bash/blob/a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b/variables.c#L1322-L1335"><code>assign_seconds</code></a>, <a href="https://github.com/bminor/bash/blob/a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b/variables.c#L1338-L1346"><code>get_seconds</code></a>). If we rely on <code>gettimeofday</code> anyway, why not rely on <code>sleep</code>?</p>
<p>Indeed, <a href="https://github.com/actions/runner/issues/3792#issuecomment-3193589914">Matthew Lugg points out</a> that this approach is not only more complex, but buggy and is vulnerable to cases where the runner is highly loaded, the system clock is adjusted and so on. A straightforward fix for the deadlock <a href="https://github.com/actions/runner/pull/3157">was eventually merged</a> - using a less-than comparator instead of equality.</p>
<p>But we are engineers, so let&rsquo;s over-engineer this whole thing by building our own <code>sleep</code> utility from the ground up.</p>
<h2 id="sticking-with-bash">Sticking with <code>bash</code></h2>
<p>To start with, let&rsquo;s sidestep the use of a realtime clock (through <code>gettimeofday</code> calls) and instead use the monotonic clock provided by the <code>bash</code> builtin <code>BASH_MONOSECONDS</code>. Introduced in <code>bash</code> 5.3 (<a href="https://lists.gnu.org/archive/html/bash-announce/2025-07/msg00000.html">release notes</a>), new and not universally available. Such a solution would look like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="nv">duration</span><span class="o">=</span><span class="nv">$1</span>
</span></span><span class="line"><span class="cl"><span class="nv">start</span><span class="o">=</span><span class="nv">$BASH_MONOSECONDS</span>
</span></span><span class="line"><span class="cl"><span class="nv">end</span><span class="o">=</span><span class="k">$((</span>start <span class="o">+</span> duration<span class="k">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="o">[[</span> <span class="nv">$BASH_MONOSECONDS</span> -lt end <span class="o">]]</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  :
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><p>Like the earlier implementation, this is a busy-wait loop. Let&rsquo;s do better and take advantage of some alternatives.</p>
<h2 id="zig-a-zig-ah">Zig-a-zig-ah</h2>
<p><a href="https://ziglang.org/">Zig</a> is a handy language for doing this kind of work - fast compilation and easy to do cross-platform work, perfect for building our own portable <code>sleep</code>.</p>
<p>First, let&rsquo;s avoid the busy-wait loop by going with an asynchronous approach. In Linux, we have <code>timefd_create</code>, <code>kqueue</code> in BSD and <code>CreateWaitableTimer</code> in Windows. Let&rsquo;s start with the <code>kqueue</code> implementation.</p>
<h3 id="kqueue">Kqueue</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="kr">const</span><span class="w"> </span><span class="n">std</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@import</span><span class="p">(</span><span class="s">&#34;std&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">fn</span><span class="w"> </span><span class="nf">sleepKqueue</span><span class="p">(</span><span class="n">ms</span><span class="o">:</span><span class="w"> </span><span class="kt">u64</span><span class="p">)</span><span class="w"> </span><span class="o">!</span><span class="kt">void</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">posix</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">std</span><span class="p">.</span><span class="n">posix</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">kq</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="nf">kqueue</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">defer</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="nf">close</span><span class="p">(</span><span class="n">kq</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">kev</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="n">Kevent</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">ident</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">filter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="n">system</span><span class="p">.</span><span class="n">EVFILT</span><span class="p">.</span><span class="n">TIMER</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">flags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="n">system</span><span class="p">.</span><span class="n">EV</span><span class="p">.</span><span class="n">ADD</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="n">system</span><span class="p">.</span><span class="n">EV</span><span class="p">.</span><span class="n">ONESHOT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">fflags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@as</span><span class="p">(</span><span class="kt">isize</span><span class="p">,</span><span class="w"> </span><span class="nb">@intCast</span><span class="p">(</span><span class="n">ms</span><span class="p">)),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">udata</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@intFromPtr</span><span class="p">(</span><span class="nb">@as</span><span class="p">(</span><span class="o">?*</span><span class="n">anyopaque</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">)),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">};</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">_</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="nf">kevent</span><span class="p">(</span><span class="n">kq</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="p">[</span><span class="n">_</span><span class="p">]</span><span class="n">posix</span><span class="p">.</span><span class="n">Kevent</span><span class="p">{</span><span class="n">kev</span><span class="p">},</span><span class="w"> </span><span class="o">&amp;</span><span class="p">[</span><span class="n">_</span><span class="p">]</span><span class="n">posix</span><span class="p">.</span><span class="n">Kevent</span><span class="p">{},</span><span class="w"> </span><span class="kc">null</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">var</span><span class="w"> </span><span class="n">out</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="n">posix</span><span class="p">.</span><span class="n">Kevent</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">undefined</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="nf">kevent</span><span class="p">(</span><span class="n">kq</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="p">[</span><span class="n">_</span><span class="p">]</span><span class="n">posix</span><span class="p">.</span><span class="n">Kevent</span><span class="p">{},</span><span class="w"> </span><span class="o">&amp;</span><span class="n">out</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">n</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="k">error</span><span class="p">.</span><span class="n">TimerWaitFailed</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p><code>kqueue</code> is the BSD equivalent of <code>epoll</code> in Linux and provides a scalable way to monitor multiple file descriptors. In our case, we&rsquo;re using it to wait for a timer event. With a bit of CLI wrapping:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ <span class="nb">time</span> ./ksleep <span class="m">2</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">real	0m2.019s
</span></span><span class="line"><span class="cl">user	0m0.003s
</span></span><span class="line"><span class="cl">sys	    0m0.008s
</span></span></code></pre></div><p>Contrast with the earlier <code>bash</code> implementation:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ <span class="nb">time</span> /tmp/badsleep.sh <span class="m">2</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">real	0m1.875s
</span></span><span class="line"><span class="cl">user	0m1.854s
</span></span><span class="line"><span class="cl">sys	    0m0.016s
</span></span></code></pre></div><p>The <code>ksleep</code> implementation is not only more efficient in CPU usage, but also more accurate in timing - we can specify millisecond precision:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ <span class="nb">time</span> ./ksleep 2.5
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">real	0m2.523s
</span></span><span class="line"><span class="cl">user	0m0.003s
</span></span><span class="line"><span class="cl">sys	    0m0.010s
</span></span></code></pre></div><p>Next, lets look at the Linux implementation with <code>timerfd</code>:</p>
<h3 id="timerfd">Timerfd</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="k">fn</span><span class="w"> </span><span class="nf">sleepTimerFd</span><span class="p">(</span><span class="n">seconds</span><span class="o">:</span><span class="w"> </span><span class="kt">f64</span><span class="p">)</span><span class="w"> </span><span class="o">!</span><span class="kt">void</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">sys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">std</span><span class="p">.</span><span class="n">os</span><span class="p">.</span><span class="n">linux</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">flags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sys</span><span class="p">.</span><span class="n">TFD</span><span class="p">{</span><span class="w"> </span><span class="p">.</span><span class="n">CLOEXEC</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">};</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">rawfd</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sys</span><span class="p">.</span><span class="nf">timerfd_create</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">timerfd_clockid_t</span><span class="p">.</span><span class="n">MONOTONIC</span><span class="p">,</span><span class="w"> </span><span class="n">flags</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">fd</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@as</span><span class="p">(</span><span class="kt">i32</span><span class="p">,</span><span class="w"> </span><span class="nb">@intCast</span><span class="p">(</span><span class="n">rawfd</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">fd</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="k">error</span><span class="p">.</span><span class="n">TimerFdCreateFailed</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">defer</span><span class="w"> </span><span class="n">std</span><span class="p">.</span><span class="n">posix</span><span class="p">.</span><span class="nf">close</span><span class="p">(</span><span class="n">fd</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">sec</span><span class="o">:</span><span class="w"> </span><span class="kt">i64</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@intFromFloat</span><span class="p">(</span><span class="nb">@floor</span><span class="p">(</span><span class="n">seconds</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">nsec</span><span class="o">:</span><span class="w"> </span><span class="kt">i64</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@intFromFloat</span><span class="p">((</span><span class="n">seconds</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nb">@as</span><span class="p">(</span><span class="kt">f64</span><span class="p">,</span><span class="w"> </span><span class="nb">@floatFromInt</span><span class="p">(</span><span class="n">sec</span><span class="p">)))</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">1_000_000_000</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">var</span><span class="w"> </span><span class="n">new_value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sys</span><span class="p">.</span><span class="n">itimerspec</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">it_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">.{</span><span class="w"> </span><span class="p">.</span><span class="n">sec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="p">.</span><span class="n">nsec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">it_value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">.{</span><span class="w"> </span><span class="p">.</span><span class="n">sec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sec</span><span class="p">,</span><span class="w"> </span><span class="p">.</span><span class="n">nsec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">nsec</span><span class="w"> </span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">};</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">no_flags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sys</span><span class="p">.</span><span class="n">TFD</span><span class="p">.</span><span class="n">TIMER</span><span class="p">{};</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="nf">timerfd_settime</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span><span class="w"> </span><span class="n">no_flags</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">new_value</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="k">error</span><span class="p">.</span><span class="n">TimerSetFailed</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">var</span><span class="w"> </span><span class="n">buf</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="mi">8</span><span class="p">]</span><span class="kt">u8</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">undefined</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="n">std</span><span class="p">.</span><span class="n">posix</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">buf</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">n</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">8</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="k">error</span><span class="p">.</span><span class="n">TimerReadFailed</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>As before, this implementation is efficient and accurate:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">root@13738b47c4e3:/ksleep# <span class="nb">time</span> ./ksleep 2.5
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">real	0m2.507s
</span></span><span class="line"><span class="cl">user	0m0.001s
</span></span><span class="line"><span class="cl">sys	    0m0.002s
</span></span></code></pre></div><p><code>timerfd_create</code> has been available since Linux 2.6.25, so it is widely supported.</p>
<h3 id="createwaitabletimer">CreateWaitableTimer</h3>
<p>I don&rsquo;t have a Windows machine, so this exercise is left up to the reader.</p>
<h2 id="wrap-up">Wrap Up</h2>
<p>It&rsquo;s not that hard to build a pretty modern <code>sleep</code> utility without relying on the existing <code>sleep</code> command. The example I&rsquo;ve shown here is not exactly production ready, but it builds statically and remains small, less than 128 KB. No need for horrible <code>bash</code> busy-wait loops.</p>
<p>Another implementation is to just call <code>nanosleep</code> directly (h/t <a href="https://bsky.app/profile/hyeon.me/post/3lx7g662pik2r">Jihyeon Kim</a>):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="k">fn</span><span class="w"> </span><span class="nf">clockNanosleep</span><span class="p">(</span><span class="n">seconds</span><span class="o">:</span><span class="w"> </span><span class="kt">f64</span><span class="p">)</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">sec</span><span class="o">:</span><span class="w"> </span><span class="kt">usize</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@intFromFloat</span><span class="p">(</span><span class="nb">@floor</span><span class="p">(</span><span class="n">seconds</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">nsec</span><span class="o">:</span><span class="w"> </span><span class="kt">usize</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@intFromFloat</span><span class="p">((</span><span class="n">seconds</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nb">@floor</span><span class="p">(</span><span class="n">seconds</span><span class="p">))</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">1_000_000_000</span><span class="p">.</span><span class="mi">0</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">posix</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">std</span><span class="p">.</span><span class="n">posix</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">posix</span><span class="p">.</span><span class="nf">nanosleep</span><span class="p">(</span><span class="n">sec</span><span class="p">,</span><span class="w"> </span><span class="n">nsec</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>But where&rsquo;s the fun in that - that&rsquo;s just what <code>sleep</code> does!</p>
<p>The final code looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="c1">// MIT License
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Copyright (c) 2025 Matthew Blewitt
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// Permission is hereby granted, free of charge, to any person obtaining a copy
</span></span></span><span class="line"><span class="cl"><span class="c1">// of this software and associated documentation files (the &#34;Software&#34;), to deal
</span></span></span><span class="line"><span class="cl"><span class="c1">// in the Software without restriction, including without limitation the rights
</span></span></span><span class="line"><span class="cl"><span class="c1">// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
</span></span></span><span class="line"><span class="cl"><span class="c1">// copies of the Software, and to permit persons to whom the Software is
</span></span></span><span class="line"><span class="cl"><span class="c1">// furnished to do so, subject to the following conditions:
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// The above copyright notice and this permission notice shall be included in all
</span></span></span><span class="line"><span class="cl"><span class="c1">// copies or substantial portions of the Software.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// THE SOFTWARE IS PROVIDED &#34;AS IS&#34;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
</span></span></span><span class="line"><span class="cl"><span class="c1">// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
</span></span></span><span class="line"><span class="cl"><span class="c1">// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
</span></span></span><span class="line"><span class="cl"><span class="c1">// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
</span></span></span><span class="line"><span class="cl"><span class="c1">// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
</span></span></span><span class="line"><span class="cl"><span class="c1">// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
</span></span></span><span class="line"><span class="cl"><span class="c1">// SOFTWARE.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kr">const</span><span class="w"> </span><span class="n">std</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@import</span><span class="p">(</span><span class="s">&#34;std&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kr">const</span><span class="w"> </span><span class="n">builtin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@import</span><span class="p">(</span><span class="s">&#34;builtin&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kr">pub</span><span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="o">!</span><span class="kt">void</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">var</span><span class="w"> </span><span class="n">buf</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="mi">512</span><span class="p">]</span><span class="kt">u8</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">undefined</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">var</span><span class="w"> </span><span class="n">fba</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">std</span><span class="p">.</span><span class="n">heap</span><span class="p">.</span><span class="n">FixedBufferAllocator</span><span class="p">.</span><span class="nf">init</span><span class="p">(</span><span class="o">&amp;</span><span class="n">buf</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">allocator</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">fba</span><span class="p">.</span><span class="nf">allocator</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">args</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="n">std</span><span class="p">.</span><span class="n">process</span><span class="p">.</span><span class="nf">argsAlloc</span><span class="p">(</span><span class="n">allocator</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">defer</span><span class="w"> </span><span class="n">std</span><span class="p">.</span><span class="n">process</span><span class="p">.</span><span class="nf">argsFree</span><span class="p">(</span><span class="n">allocator</span><span class="p">,</span><span class="w"> </span><span class="n">args</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">len</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">2</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">std</span><span class="p">.</span><span class="n">debug</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;Usage: {s} &lt;seconds&gt;</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span><span class="w"> </span><span class="p">.{</span><span class="n">args</span><span class="p">[</span><span class="mi">0</span><span class="p">]});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">std</span><span class="p">.</span><span class="n">process</span><span class="p">.</span><span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">seconds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">std</span><span class="p">.</span><span class="n">fmt</span><span class="p">.</span><span class="nf">parseFloat</span><span class="p">(</span><span class="kt">f64</span><span class="p">,</span><span class="w"> </span><span class="n">args</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">std</span><span class="p">.</span><span class="n">debug</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;Invalid number: {s}</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span><span class="w"> </span><span class="p">.{</span><span class="n">args</span><span class="p">[</span><span class="mi">1</span><span class="p">]});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">std</span><span class="p">.</span><span class="n">process</span><span class="p">.</span><span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">};</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">seconds</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">std</span><span class="p">.</span><span class="n">debug</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;Duration must be &gt;= 0</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span><span class="w"> </span><span class="p">.{});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">std</span><span class="p">.</span><span class="n">process</span><span class="p">.</span><span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">millis</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@as</span><span class="p">(</span><span class="kt">u64</span><span class="p">,</span><span class="w"> </span><span class="nb">@intFromFloat</span><span class="p">(</span><span class="n">seconds</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">1000.0</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">switch</span><span class="w"> </span><span class="p">(</span><span class="n">builtin</span><span class="p">.</span><span class="n">os</span><span class="p">.</span><span class="n">tag</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">linux</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="nf">sleepTimerFd</span><span class="p">(</span><span class="n">seconds</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">macos</span><span class="p">,</span><span class="w"> </span><span class="p">.</span><span class="n">ios</span><span class="p">,</span><span class="w"> </span><span class="p">.</span><span class="n">freebsd</span><span class="p">,</span><span class="w"> </span><span class="p">.</span><span class="n">netbsd</span><span class="p">,</span><span class="w"> </span><span class="p">.</span><span class="n">openbsd</span><span class="p">,</span><span class="w"> </span><span class="p">.</span><span class="n">dragonfly</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="nf">sleepKqueue</span><span class="p">(</span><span class="n">millis</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">else</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">std</span><span class="p">.</span><span class="n">debug</span><span class="p">.</span><span class="nf">print</span><span class="p">(</span><span class="s">&#34;Unsupported OS: {s}</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span><span class="w"> </span><span class="p">.{</span><span class="n">builtin</span><span class="p">.</span><span class="n">os</span><span class="p">.</span><span class="n">tag</span><span class="p">});</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">std</span><span class="p">.</span><span class="n">process</span><span class="p">.</span><span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">fn</span><span class="w"> </span><span class="nf">sleepKqueue</span><span class="p">(</span><span class="n">ms</span><span class="o">:</span><span class="w"> </span><span class="kt">u64</span><span class="p">)</span><span class="w"> </span><span class="o">!</span><span class="kt">void</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">posix</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">std</span><span class="p">.</span><span class="n">posix</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">kq</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="nf">kqueue</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">defer</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="nf">close</span><span class="p">(</span><span class="n">kq</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">kev</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="n">Kevent</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">ident</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">filter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="n">system</span><span class="p">.</span><span class="n">EVFILT</span><span class="p">.</span><span class="n">TIMER</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">flags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="n">system</span><span class="p">.</span><span class="n">EV</span><span class="p">.</span><span class="n">ADD</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="n">system</span><span class="p">.</span><span class="n">EV</span><span class="p">.</span><span class="n">ONESHOT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">fflags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@as</span><span class="p">(</span><span class="kt">isize</span><span class="p">,</span><span class="w"> </span><span class="nb">@intCast</span><span class="p">(</span><span class="n">ms</span><span class="p">)),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">udata</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@intFromPtr</span><span class="p">(</span><span class="nb">@as</span><span class="p">(</span><span class="o">?*</span><span class="n">anyopaque</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">)),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">};</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">_</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="nf">kevent</span><span class="p">(</span><span class="n">kq</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="p">[</span><span class="n">_</span><span class="p">]</span><span class="n">posix</span><span class="p">.</span><span class="n">Kevent</span><span class="p">{</span><span class="n">kev</span><span class="p">},</span><span class="w"> </span><span class="o">&amp;</span><span class="p">[</span><span class="n">_</span><span class="p">]</span><span class="n">posix</span><span class="p">.</span><span class="n">Kevent</span><span class="p">{},</span><span class="w"> </span><span class="kc">null</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">var</span><span class="w"> </span><span class="n">out</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="n">posix</span><span class="p">.</span><span class="n">Kevent</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">undefined</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="n">posix</span><span class="p">.</span><span class="nf">kevent</span><span class="p">(</span><span class="n">kq</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="p">[</span><span class="n">_</span><span class="p">]</span><span class="n">posix</span><span class="p">.</span><span class="n">Kevent</span><span class="p">{},</span><span class="w"> </span><span class="o">&amp;</span><span class="n">out</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">n</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="k">error</span><span class="p">.</span><span class="n">TimerWaitFailed</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">fn</span><span class="w"> </span><span class="nf">sleepTimerFd</span><span class="p">(</span><span class="n">seconds</span><span class="o">:</span><span class="w"> </span><span class="kt">f64</span><span class="p">)</span><span class="w"> </span><span class="o">!</span><span class="kt">void</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">sys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">std</span><span class="p">.</span><span class="n">os</span><span class="p">.</span><span class="n">linux</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">flags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sys</span><span class="p">.</span><span class="n">TFD</span><span class="p">{</span><span class="w"> </span><span class="p">.</span><span class="n">CLOEXEC</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">};</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">rawfd</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sys</span><span class="p">.</span><span class="nf">timerfd_create</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">timerfd_clockid_t</span><span class="p">.</span><span class="n">MONOTONIC</span><span class="p">,</span><span class="w"> </span><span class="n">flags</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">fd</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@as</span><span class="p">(</span><span class="kt">i32</span><span class="p">,</span><span class="w"> </span><span class="nb">@intCast</span><span class="p">(</span><span class="n">rawfd</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">fd</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="k">error</span><span class="p">.</span><span class="n">TimerFdCreateFailed</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">defer</span><span class="w"> </span><span class="n">std</span><span class="p">.</span><span class="n">posix</span><span class="p">.</span><span class="nf">close</span><span class="p">(</span><span class="n">fd</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">sec</span><span class="o">:</span><span class="w"> </span><span class="kt">i64</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@intFromFloat</span><span class="p">(</span><span class="nb">@floor</span><span class="p">(</span><span class="n">seconds</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">nsec</span><span class="o">:</span><span class="w"> </span><span class="kt">i64</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">@intFromFloat</span><span class="p">((</span><span class="n">seconds</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nb">@as</span><span class="p">(</span><span class="kt">f64</span><span class="p">,</span><span class="w"> </span><span class="nb">@floatFromInt</span><span class="p">(</span><span class="n">sec</span><span class="p">)))</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">1_000_000_000</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">var</span><span class="w"> </span><span class="n">new_value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sys</span><span class="p">.</span><span class="n">itimerspec</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">it_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">.{</span><span class="w"> </span><span class="p">.</span><span class="n">sec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="p">.</span><span class="n">nsec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">it_value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">.{</span><span class="w"> </span><span class="p">.</span><span class="n">sec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sec</span><span class="p">,</span><span class="w"> </span><span class="p">.</span><span class="n">nsec</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">nsec</span><span class="w"> </span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">};</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">no_flags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sys</span><span class="p">.</span><span class="n">TFD</span><span class="p">.</span><span class="n">TIMER</span><span class="p">{};</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="nf">timerfd_settime</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span><span class="w"> </span><span class="n">no_flags</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">new_value</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="k">error</span><span class="p">.</span><span class="n">TimerSetFailed</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">var</span><span class="w"> </span><span class="n">buf</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="mi">8</span><span class="p">]</span><span class="kt">u8</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">undefined</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">const</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="n">std</span><span class="p">.</span><span class="n">posix</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">buf</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">n</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">8</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="k">error</span><span class="p">.</span><span class="n">TimerReadFailed</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div>]]></content:encoded></item><item><title>Embodiment</title><link>https://matt.blwt.io/post/embodiment/</link><pubDate>Thu, 15 May 2025 21:58:18 +0100</pubDate><guid>https://matt.blwt.io/post/embodiment/</guid><description>&lt;p&gt;Since I was small, I&amp;rsquo;d lived my life through screens, one way or another. For the last quarter-century or so, I had been piloting the machine of flesh and sinew to keep my mind going, rarely engaging with the machine itself. This was a mistake.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>Since I was small, I&rsquo;d lived my life through screens, one way or another. For the last quarter-century or so, I had been piloting the machine of flesh and sinew to keep my mind going, rarely engaging with the machine itself. This was a mistake.</p>
<p>The machine was necessary, but unremarkable. It tolerated the relative abuse I&rsquo;d put it through, really through <em>inaction</em> rather than specific events. I rarely sought out physical sensation - the mind was far more interesting. It won&rsquo;t surprise you to learn I was a bookworm.</p>
<figure><img src="/embodiment/header.webp">
</figure>

<p>Over the last couple of years, I&rsquo;d felt a dissonance start to amplify, and this came to a head with the loss of my father-in-law. A little cliché that a death in the family forces you to stare at your own mortality. The machine was fallible. As much as I enjoy <a href="/post/a-love-letter-to-giant-robots">a good mecha story</a>, I really needed to be <em>in</em> the world than just piloting the machine and experiencing life in the abstract.</p>
<p><a href="https://plato.stanford.edu/entries/embodied-cognition/"><em>Embodied cognition</em> </a> teaches us that thought emerges only in the full context of <em>the body</em> - the collection of organs that sense the world and ground us in it. <a href="https://en.wikipedia.org/wiki/Maurice_Merleau-Ponty">Merleau-Ponty</a>, godfather of phenomenology, takes this even further - <em>&ldquo;Truth does not inhabit only the inner man, or more accurately, there is no inner man, man is in the world, and only in the world does he know himself.&rdquo;</em></p>
<p>Screens had compressed the world. From my family computer in the dinging room, to my personal laptops and smartphones (a term that now feels <em>very</em> dated), my first relationships formed. I was a richer, more vibrant person on the internet - the person I felt I <em>truly</em> was. The compression also compressed my thoughts, being ignorant to how powerful the &ldquo;shower thought&rdquo; was, or the thoughts I had while in those liminal spaces between the screen and whatever necessary location I had to pilot my machine to.</p>
<p>After tending to my father-in-law&rsquo;s affairs, I resolved to stop living in my head quite so much. Partially out of fear of the inevitable, partially forced to reckon that I was no longer a twenty-something with unlimited possibilities and all the time to explore them.</p>
<p>I experienced a kind of humility in rejoining my body after years away. The nostalgia of climbing over rocks and feeling sand between my toes, combined with the acute realisation that you are capable of both more and less than you used to be. I realised that my body had held on to <em>so much</em>–tension long ignored, fatigue overruled, postponed joy and deferred sadness. It had always been trying to communicate these things, but I just wasn&rsquo;t listening. <a href="https://app.thestorygraph.com/books/63815d20-e57d-480e-ac68-6a56fa0dc0bc">The body keeps the score</a> indeed.</p>
<p>A few things changed. The first was moving with intent. I started with walking, then <a href="/post/movement-for-engineers/">kettlebells</a>, and now my regular routine of exercise 6 days a week. Running, oddly enough, is the thing that has resonated with me the most, with lifting being an on-again-off-again relationship I&rsquo;ve had for a long time. I was listening to my body, for what felt like the first time.</p>
<p>I began to think differently—not just <em>with</em> different thoughts, but <em>through</em> a different mode of being. Less control, more noticing. Less analysis, more relation. The world became thicker, <em>stranger</em>, more alive. I wasn&rsquo;t watching it any more. I was in it.</p>
<p>I am no longer just a pilot. I am the machine, and the machine is me, being in the world with all we&rsquo;ve got.</p>
<p>In a world being rapidly changed by simulacra of minds, being truly embodied has shown how stark the differences between our minds and these simulacra are. They can put on a good show, but they aren&rsquo;t concious. Can a disembodied mind even perform things like spatial reasoning, with no concept of &ldquo;space&rdquo;? What would happen if there was an embodied simulacrum?</p>
<p>What if an LLM could touch grass?</p>]]></content:encoded></item><item><title>Leadership Power Tools: SQL and Statistics</title><link>https://matt.blwt.io/post/leadership-power-tools-sql-and-statistics/</link><pubDate>Wed, 04 Dec 2024 10:00:00 +0000</pubDate><guid>https://matt.blwt.io/post/leadership-power-tools-sql-and-statistics/</guid><description>&lt;p&gt;A common pattern I&amp;rsquo;ve seen over the years have been folks in engineering leadership positions that are not super comfortable with extracting and interpreting data from stores, be it databases, CSV files in an object store, or even just a spreadsheet. We&amp;rsquo;re going to cover SQL &amp;amp; DuckDB, then some useful statistical tools: summary stats, distributions, confidence intervals and Bayesian reasoning.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>A common pattern I&rsquo;ve seen over the years have been folks in engineering leadership positions that are not super comfortable with extracting and interpreting data from stores, be it databases, CSV files in an object store, or even just a spreadsheet. We&rsquo;re going to cover SQL &amp; DuckDB, then some useful statistical tools: summary stats, distributions, confidence intervals and Bayesian reasoning.</p>
<p>All too often a request is put into an BI team or data analyst to &ldquo;run a report&rdquo; or something similar. Instead, pick up some power tools and <a href="https://www.youtube.com/watch?v=vOBr0jcqnZo">do it yourself</a>!</p>
<figure><img src="/leadership-power-tools-sql-and-statistics/header.webp"
    alt="A line drawing of a power drill and table saw, continuing the power tools metaphor.">
</figure>

<h2 id="table-of-contents">Table of Contents</h2>
<ul>
<li><a href="#sql-and-duckdb">SQL and DuckDB</a></li>
<li><a href="#statistics">Statistics</a>
<ul>
<li><a href="#applying-summary-statistics">Applying Summary Statistics</a></li>
<li><a href="#confidence-intervals">Confidence Intervals</a></li>
<li><a href="#bayesian-reasoning">Bayesian Reasoning</a></li>
</ul>
</li>
<li><a href="#wrap-up">Wrap Up</a></li>
</ul>
<h2 id="sql-and-duckdb">SQL and DuckDB</h2>
<p><a href="https://duckdb.org/">DuckDB</a> is probably going to be your best entry point into the world of SQL. It&rsquo;s a lightweight, in-process SQL database that you can use to run queries against CSV files, Parquet files, and more. It&rsquo;s a great way to get started with SQL without having to worry about setting up a database server or anything like that. It can also connect to remote databases, <a href="https://duckdb.org/docs/guides/database_integration/postgres">like Postgres</a> or <a href="https://duckdb.org/docs/guides/database_integration/mysql">MySQL</a>.</p>
<p>If you aren&rsquo;t familiar with SQL, I&rsquo;d recommend going through a brief tutorial - <a href="https://www.sqltutorial.org/">SQLTutorial</a> is a pretty good place to start - look at using <a href="https://www.sqlite.org/index.html">SQLite</a> as a database to practice with. Once you&rsquo;re comfortable with the basics, you can start using DuckDB to run queries against your data.</p>
<p>SQL is our <em>lingua franca</em> for data querying. Knowing how to write SQL queries is a powerful tool to have in your toolbox, and <em>will</em> make you more effective. It&rsquo;s not just for data analysts or BI folks - it&rsquo;s for anyone who needs to make decisions based on data. I&rsquo;d recommend getting familiar with <code>JOIN</code>s, <code>GROUP BY</code>, and <code>HAVING</code> clauses, as well as window functions. If you want to push the envelope, look into CTEs (Common Table Expressions) and recursive queries.</p>
<p>Lets say you have an extract of data from your production database, and you want to find out how many users signed up per day over the last month. Given a CSV file <code>users.csv</code> with the following columns:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csv" data-lang="csv"><span class="line"><span class="cl"><span class="s">user_id</span><span class="p">,</span><span class="s">created_at</span><span class="p">
</span></span></span><span class="line"><span class="cl"><span class="p"></span><span class="s">1</span><span class="p">,</span><span class="s">2024-11-01</span><span class="p">
</span></span></span><span class="line"><span class="cl"><span class="p"></span><span class="s">2</span><span class="p">,</span><span class="s">2024-11-01</span><span class="p">
</span></span></span><span class="line"><span class="cl"><span class="p"></span><span class="s">3</span><span class="p">,</span><span class="s">2024-11-02</span><span class="p">
</span></span></span><span class="line"><span class="cl"><span class="p"></span><span class="s">4</span><span class="p">,</span><span class="s">2024-11-02</span><span class="p">
</span></span></span><span class="line"><span class="cl"><span class="p"></span><span class="s">5</span><span class="p">,</span><span class="s">2024-11-02</span><span class="p">
</span></span></span><span class="line"><span class="cl"><span class="p"></span><span class="s">6</span><span class="p">,</span><span class="s">2024-11-03</span><span class="p">
</span></span></span></code></pre></div><p>You can run the following query in DuckDB:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">created_at</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">COUNT</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">signups</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="s2">&#34;users.csv&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">created_at</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="k">ASC</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>This will give you a table with the number of signups per day:</p>
<div class="monotable">
<table>
  <thead>
      <tr>
          <th>created_at</th>
          <th style="text-align: right">signups</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2024-11-01</td>
          <td style="text-align: right">2</td>
      </tr>
      <tr>
          <td>2024-11-02</td>
          <td style="text-align: right">3</td>
      </tr>
      <tr>
          <td>2024-11-03</td>
          <td style="text-align: right">1</td>
      </tr>
  </tbody>
</table>
</div>
<p>A trivial example, but an example all the same. Now we can get the data and do some surface level aggregations, lets start working through some statistical methods. I&rsquo;m going to cover 5 statistical methods that I think are especially useful for leaders: summary statistics, distributions, confidence intervals and Bayesian reasoning.</p>
<h2 id="statistics">Statistics</h2>
<p>Sorry, R fans - Python is my go-to for analysis. Before we get into the specific methods, lets talk a little about why you should bother with this stuff at all. I&rsquo;m a big believer in <a href="https://datadriven.club/">data-informed decisions</a>, and to do that we need ways to interpret the data and talk about it authoritatively. Statistical methods are your other set of tools.</p>
<p>Here&rsquo;s a quick example, levering DuckDB again, but this time for <a href="https://duckdb.org/docs/guides/meta/summarize.html">summary statistics</a>. Let&rsquo;s grab some sample data from <a href="https://datasets.imdbws.com/">IMDB Non-Commercial Datasets</a> - <code>title.ratings.tsv.gz</code> - and <code>SUMMARIZE</code> it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">column_name</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">col</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">approx_unique</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">uniq</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">round</span><span class="p">(</span><span class="k">avg</span><span class="p">::</span><span class="n">double</span><span class="p">,</span><span class="mi">2</span><span class="p">)</span><span class="w"> </span><span class="k">avg</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">min</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">round</span><span class="p">(</span><span class="n">q25</span><span class="p">::</span><span class="n">double</span><span class="p">,</span><span class="mi">2</span><span class="p">)</span><span class="w"> </span><span class="n">q25</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">round</span><span class="p">(</span><span class="n">q50</span><span class="p">::</span><span class="n">double</span><span class="p">,</span><span class="mi">2</span><span class="p">)</span><span class="w"> </span><span class="n">q50</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">round</span><span class="p">(</span><span class="n">q75</span><span class="p">::</span><span class="n">double</span><span class="p">,</span><span class="mi">2</span><span class="p">)</span><span class="w"> </span><span class="n">q75</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">max</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="p">(</span><span class="n">SUMMARIZE</span><span class="w"> </span><span class="s2">&#34;title.ratings.tsv.gz&#34;</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><div class="monotable">
<table>
  <thead>
      <tr>
          <th>col</th>
          <th>uniq</th>
          <th>avg</th>
          <th>min</th>
          <th>q25</th>
          <th>q50</th>
          <th>q75</th>
          <th>max</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>tconst</td>
          <td>1394000</td>
          <td></td>
          <td>tt0000001</td>
          <td></td>
          <td></td>
          <td></td>
          <td>tt9916880</td>
      </tr>
      <tr>
          <td>averageRating</td>
          <td>73</td>
          <td>6.95</td>
          <td>1.0</td>
          <td>6.2</td>
          <td>7.15</td>
          <td>7.9</td>
          <td>10.0</td>
      </tr>
      <tr>
          <td>numVotes</td>
          <td>23695</td>
          <td>1027.92</td>
          <td>5</td>
          <td>11.0</td>
          <td>26.0</td>
          <td>100.0</td>
          <td>2966204</td>
      </tr>
  </tbody>
</table>
</div>
<p>Descriptive summary statistics like this, including all-important <a href="https://en.wikipedia.org/wiki/Percentile">percentiles</a>, give you a quick overview of the data. Here, we can see we&rsquo;ve got almost 1.4M entries, but only 73 unique ratings between 1.0 and 10.0. We can also see that although the <em>mean average</em> is 6.95, the <em>median</em> is 7.15.</p>
<p>I generally prefer using percentiles when discussing data like this. For example, saying something like &ldquo;an above average number of 5xx error codes&rdquo; is not particularly illuminating, but saying &ldquo;an increase of 10% in the p95 count of 5xx error codes month on month&rdquo; is more precise. For more on this, see <a href="https://sled.rs/perf.html#e-prime-and-precise-language">Tyler Neely&rsquo;s notes on E-Prime and precise language</a>.</p>
<h3 id="applying-summary-statistics">Applying Summary Statistics</h3>
<p>Lets have a look at something a little more interesting, and maybe more directly relevant to engineering leaders - issue tracker data. I&rsquo;m going to be using a cleaned dataset from <a href="https://github.com/logpai/bughub"><code>logpai/bughub</code></a> for Firefox. First of all, let&rsquo;s load it into DuckDB and get an overview on what we&rsquo;re looking at:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">DESCRIBE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="s1">&#39;mozilla_firefox.csv&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><div class="monotable">
<table>
  <thead>
      <tr>
          <th>column_name</th>
          <th>column_type</th>
          <th>null</th>
          <th>key</th>
          <th>default</th>
          <th>extra</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Issue_id</td>
          <td>BIGINT</td>
          <td>YES</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Priority</td>
          <td>VARCHAR</td>
          <td>YES</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Component</td>
          <td>VARCHAR</td>
          <td>YES</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Duplicated_issue</td>
          <td>DOUBLE</td>
          <td>YES</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Title</td>
          <td>VARCHAR</td>
          <td>YES</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Description</td>
          <td>VARCHAR</td>
          <td>YES</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Status</td>
          <td>VARCHAR</td>
          <td>YES</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Resolution</td>
          <td>VARCHAR</td>
          <td>YES</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Version</td>
          <td>VARCHAR</td>
          <td>YES</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Created_time</td>
          <td>VARCHAR</td>
          <td>YES</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Resolved_time</td>
          <td>VARCHAR</td>
          <td>YES</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
  </tbody>
</table>
</div>
<p>Alright, <code>Duplicated_issue</code> is the wrong type given it should be referring to <code>Issue_id</code>, but nothing we can&rsquo;t work with.</p>
<p>We don&rsquo;t have some ready made values to summarize, so we&rsquo;ll have to derive some ourselves. Instead, lets break down time to resolve by Priority, and then by Component, excluding duplicates and non-resolved issues. We need to do a bit of cleanup with timestamp <code>varchar</code> fields first, though, as they aren&rsquo;t in RFC 3339 format:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">csv</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">replace_timestamps</span><span class="p">(</span><span class="n">file</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">reader</span> <span class="o">=</span> <span class="n">csv</span><span class="o">.</span><span class="n">reader</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">header</span> <span class="o">=</span> <span class="nb">next</span><span class="p">(</span><span class="n">reader</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">rows</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">reader</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">rows</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="p">(</span><span class="mi">9</span><span class="p">,</span> <span class="mi">10</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">==</span> <span class="s2">&#34;&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">continue</span>
</span></span><span class="line"><span class="cl">            <span class="n">timestamp</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">strptime</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="s2">&#34;%Y-%m-</span><span class="si">%d</span><span class="s2"> %H:%M:%S %z&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">row</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">timestamp</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&#34;%Y-%m-</span><span class="si">%d</span><span class="s2">T%H:%M:%S%z&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="s2">&#34;w&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">writer</span> <span class="o">=</span> <span class="n">csv</span><span class="o">.</span><span class="n">writer</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">writer</span><span class="o">.</span><span class="n">writerow</span><span class="p">(</span><span class="n">header</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">writer</span><span class="o">.</span><span class="n">writerows</span><span class="p">(</span><span class="n">rows</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">replace_timestamps</span><span class="p">(</span><span class="s2">&#34;mozilla_firefox.csv&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Then load this into DuckDB.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- We need to install this extension first to handle timezones.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">install</span><span class="w"> </span><span class="n">ICU</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">load</span><span class="w"> </span><span class="n">ICU</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- I prefer CTEs over subqueries
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">with</span><span class="w"> </span><span class="n">filtered</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">select</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="s2">&#34;Priority&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">(</span><span class="s2">&#34;Resolved_time&#34;</span><span class="p">::</span><span class="n">timestamptz</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="s2">&#34;Created_time&#34;</span><span class="p">::</span><span class="n">timestamptz</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">time_to_resolve</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">from</span><span class="w"> </span><span class="s2">&#34;mozilla_firefox.csv&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">where</span><span class="w"> </span><span class="s2">&#34;Status&#34;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;RESOLVED&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">and</span><span class="w"> </span><span class="s2">&#34;Resolution&#34;</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="s1">&#39;DUPLICATE&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">select</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;Priority&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">quantile_disc</span><span class="p">(</span><span class="n">time_to_resolve</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">.</span><span class="mi">25</span><span class="w"> </span><span class="k">order</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="s2">&#34;Priority&#34;</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">p25_ttr</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">quantile_disc</span><span class="p">(</span><span class="n">time_to_resolve</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">.</span><span class="mi">50</span><span class="w"> </span><span class="k">order</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="s2">&#34;Priority&#34;</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">p50_ttr</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">quantile_disc</span><span class="p">(</span><span class="n">time_to_resolve</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">.</span><span class="mi">75</span><span class="w"> </span><span class="k">order</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="s2">&#34;Priority&#34;</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">p75_ttr</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">quantile_disc</span><span class="p">(</span><span class="n">time_to_resolve</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">.</span><span class="mi">95</span><span class="w"> </span><span class="k">order</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="s2">&#34;Priority&#34;</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">p95_ttr</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">from</span><span class="w"> </span><span class="n">filtered</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">group</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="s2">&#34;Priority&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">order</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="s2">&#34;Priority&#34;</span><span class="w"> </span><span class="k">ASC</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><div class="monotable">
<table>
  <thead>
      <tr>
          <th>Priority</th>
          <th>p25_ttr</th>
          <th>p50_ttr</th>
          <th>p75_ttr</th>
          <th>p95_ttr</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>&ndash;</td>
          <td>6 days 04:17:51</td>
          <td>176 days 15:37:52</td>
          <td>622 days 20:05:33</td>
          <td>1463 days 15:53:53</td>
      </tr>
      <tr>
          <td>P1</td>
          <td>8 days 04:43:33</td>
          <td>64 days 03:00:19</td>
          <td>511 days 06:08:24</td>
          <td>1416 days 01:15:26</td>
      </tr>
      <tr>
          <td>P2</td>
          <td>38 days 01:14:05</td>
          <td>204 days 00:29:32</td>
          <td>895 days 15:40:19</td>
          <td>1450 days 18:11:01</td>
      </tr>
      <tr>
          <td>P3</td>
          <td>56 days 04:59:32</td>
          <td>243 days 18:57:23</td>
          <td>830 days 03:02:28</td>
          <td>1500 days 18:38:30</td>
      </tr>
      <tr>
          <td>P4</td>
          <td>83 days 23:55:59</td>
          <td>508 days 04:25:40</td>
          <td>1170 days 08:47:13</td>
          <td>2568 days 02:57:14</td>
      </tr>
      <tr>
          <td>P5</td>
          <td>26 days 00:55:24</td>
          <td>416 days 05:43:22</td>
          <td>1356 days 05:23:26</td>
          <td>2800 days 01:20:26</td>
      </tr>
  </tbody>
</table>
</div>
<p>Neat. And using a similar query, lets look at it by Component:</p>
<div class="monotable">
<table>
  <thead>
      <tr>
          <th>Component</th>
          <th>p25_ttr</th>
          <th>p50_ttr</th>
          <th>p75_ttr</th>
          <th>p95_ttr</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>General</td>
          <td>8 days 16:47:07</td>
          <td>256 days 13:06:32</td>
          <td>626 days 11:20:33</td>
          <td>1354 days 11:48:16</td>
      </tr>
      <tr>
          <td>Bookmarks &amp; History</td>
          <td>198 days 21:35:22</td>
          <td>691 days 08:23:16</td>
          <td>1126 days 11:19:23</td>
          <td>1705 days 18:23:34</td>
      </tr>
      <tr>
          <td>Untriaged</td>
          <td>03:04:44</td>
          <td>1 day 15:08:14</td>
          <td>61 days 05:53:53</td>
          <td>485 days 00:03:59</td>
      </tr>
      <tr>
          <td>Tabbed Browser</td>
          <td>8 days 01:04:35</td>
          <td>201 days 14:07:15</td>
          <td>719 days 14:09:05</td>
          <td>2028 days 12:20:09</td>
      </tr>
      <tr>
          <td>Toolbars and Customization</td>
          <td>20 days 08:06:01</td>
          <td>186 days 06:34:57</td>
          <td>836 days 12:22:37</td>
          <td>1952 days 19:43:21</td>
      </tr>
      <tr>
          <td>[snip]</td>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
  </tbody>
</table>
</div>
<h4 id="distributions-and-histograms">Distributions And Histograms</h4>
<p>DuckDB can also help us with reviewing histograms, before moving into using something like a Python notebook to render it out as a graph. Lets say we wanted to review the distribution of all the P1 issue closures:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">with</span><span class="w"> </span><span class="n">filtered</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">select</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">round</span><span class="p">(</span><span class="n">epoch</span><span class="p">((</span><span class="s2">&#34;Resolved_time&#34;</span><span class="p">::</span><span class="n">timestamptz</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="s2">&#34;Created_time&#34;</span><span class="p">::</span><span class="n">timestamptz</span><span class="p">))</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mi">3600</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">time_to_resolve_hours</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">from</span><span class="w"> </span><span class="s2">&#34;mozilla_firefox.csv&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">where</span><span class="w"> </span><span class="s2">&#34;Status&#34;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;RESOLVED&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">and</span><span class="w"> </span><span class="s2">&#34;Resolution&#34;</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="s1">&#39;DUPLICATE&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">and</span><span class="w"> </span><span class="s2">&#34;Priority&#34;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;P1&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">select</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">histogram</span><span class="p">(</span><span class="n">filtered</span><span class="p">,</span><span class="w"> </span><span class="n">time_to_resolve_hours</span><span class="p">,</span><span class="w"> </span><span class="n">bin_count</span><span class="p">:</span><span class="o">=</span><span class="mi">15</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><p>This gives us an inline bar-chart:</p>
<div class="monotable">
<table>
  <thead>
      <tr>
          <th>bin</th>
          <th>count</th>
          <th>bar</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>x &lt;= 5000.0</td>
          <td>607</td>
          <td>████████████████████████████████████████████</td>
      </tr>
      <tr>
          <td>5000.0 &lt; x &lt;= 10000.0</td>
          <td>73</td>
          <td>█████████▌</td>
      </tr>
      <tr>
          <td>10000.0 &lt; x &lt;= 15000.0</td>
          <td>55</td>
          <td>███████▏</td>
      </tr>
      <tr>
          <td>15000.0 &lt; x &lt;= 20000.0</td>
          <td>27</td>
          <td>███▌</td>
      </tr>
      <tr>
          <td>20000.0 &lt; x &lt;= 25000.0</td>
          <td>27</td>
          <td>███▌</td>
      </tr>
      <tr>
          <td>25000.0 &lt; x &lt;= 30000.0</td>
          <td>24</td>
          <td>███▏</td>
      </tr>
      <tr>
          <td>30000.0 &lt; x &lt;= 35000.0</td>
          <td>81</td>
          <td>██████████▋</td>
      </tr>
      <tr>
          <td>35000.0 &lt; x &lt;= 40000.0</td>
          <td>15</td>
          <td>█▉</td>
      </tr>
      <tr>
          <td>40000.0 &lt; x &lt;= 45000.0</td>
          <td>5</td>
          <td>▋</td>
      </tr>
      <tr>
          <td>45000.0 &lt; x &lt;= 50000.0</td>
          <td>1</td>
          <td>▏</td>
      </tr>
      <tr>
          <td>50000.0 &lt; x &lt;= 55000.0</td>
          <td>4</td>
          <td>▌</td>
      </tr>
      <tr>
          <td>55000.0 &lt; x &lt;= 60000.0</td>
          <td>2</td>
          <td>▎</td>
      </tr>
      <tr>
          <td>60000.0 &lt; x &lt;= 65000.0</td>
          <td>8</td>
          <td>█</td>
      </tr>
      <tr>
          <td>65000.0 &lt; x &lt;= 70000.0</td>
          <td>2</td>
          <td>▎</td>
      </tr>
      <tr>
          <td>70000.0 &lt; x &lt;= 75000.0</td>
          <td>0</td>
          <td></td>
      </tr>
      <tr>
          <td>75000.0 &lt; x &lt;= 80000.0</td>
          <td>1</td>
          <td>▏</td>
      </tr>
      <tr>
          <td>80000.0 &lt; x &lt;= 85000.0</td>
          <td>1</td>
          <td>▏</td>
      </tr>
  </tbody>
</table>
</div>
<p>If you tilt your head to the side, you can see this data exhibits a strong <a href="https://en.wikipedia.org/wiki/Pareto_distribution">Pareto distribution</a>, where ~80% of the entries fall within the first bucket of taking up to 5000 hours / 208 days to complete. A larger <code>bin_count</code> would demonstrate a further breakdown.</p>
<p>In my mind, there are about 5 core distributions that you should recognise by sight:</p>
<ul>
<li>Pareto distribution and associated <a href="https://en.wikipedia.org/wiki/Power_law">power law</a></li>
<li><a href="https://en.wikipedia.org/wiki/Normal_distribution">Gaussian distribution</a></li>
<li><a href="https://en.wikipedia.org/wiki/Gamma_distribution">Gamma distribution</a></li>
<li><a href="https://en.wikipedia.org/wiki/Log-normal_distribution">Log-normal distribution</a></li>
<li><a href="https://en.wikipedia.org/wiki/Beta_distribution">Beta distribution</a></li>
</ul>
<p>By being able to grasp probabilistic thinking, this allows you to make better predictions and &ldquo;be right more often&rdquo;.</p>
<h3 id="confidence-intervals">Confidence Intervals</h3>
<p>As an engineering leader, you are going to need to be right a lot. Part of that is coming up with high-confidence estimations. The very best seem to have an almost magical way of providing high quality estimates out of thin air. For the rest of us, there are statistical methods.</p>
<p>Given our historical data above, imagine a Firefox PM wanting to know how long it will take for us to solve a bug in our &ldquo;Tabbed Browser&rdquo; component. We&rsquo;ll cover two sides of this - calculating the confidence interval, and using a Monte Carlo simulation.</p>
<h4 id="lets-do-maths">Lets Do Maths</h4>
<p><a href="https://en.wikipedia.org/wiki/Confidence_interval">Confidence intervals</a> have a few different ways of calculating. As we know from above, our data is not normally distributed. Fortunately, we have enough samples to take advantage of the <a href="https://en.wikipedia.org/wiki/Central_limit_theorem">Central Limit Theorem</a> and the distribution of the mean.</p>
<p>This lets us use the following formula:</p>
<pre tabindex="0"><code>CI = Mean ± Z * (stddev / √n)
</code></pre><p>where:</p>
<ul>
<li>Z is the z-statistic 1.96 for 95% confidence</li>
<li>stddev is the standard deviation</li>
<li>n is the number of samples</li>
</ul>
<p>The confidence interval is best given as a tuple of the upper and lower bound. Lets prepare our data with DuckDB:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">COPY</span><span class="w"> </span><span class="p">(</span><span class="k">select</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;Issue_id&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">round</span><span class="p">(</span><span class="n">epoch</span><span class="p">((</span><span class="s2">&#34;Resolved_time&#34;</span><span class="p">::</span><span class="n">timestamptz</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="s2">&#34;Created_time&#34;</span><span class="p">::</span><span class="n">timestamptz</span><span class="p">))</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mi">3600</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">time_to_resolve_hours</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">from</span><span class="w"> </span><span class="s2">&#34;mozilla_firefox.csv&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">where</span><span class="w"> </span><span class="s2">&#34;Status&#34;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;RESOLVED&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">and</span><span class="w"> </span><span class="s2">&#34;Resolution&#34;</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="s1">&#39;DUPLICATE&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">and</span><span class="w"> </span><span class="s2">&#34;Component&#34;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Tabbed Browser&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s1">&#39;tabbed_browser_ttr.csv&#39;</span><span class="w"> </span><span class="p">(</span><span class="n">header</span><span class="p">,</span><span class="w"> </span><span class="k">delimiter</span><span class="w"> </span><span class="s1">&#39;,&#39;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">select</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="s1">&#39;tabbed_browser_ttr.csv&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><div class="monotable">
<table>
  <thead>
      <tr>
          <th>count_star()</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2737</td>
      </tr>
  </tbody>
</table>
</div>
<p>Now we can leverage Python and <code>polars</code> to calculate our confidence interval:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars</span> <span class="k">as</span> <span class="nn">pl</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">pl</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s2">&#34;tabbed_browser_ttr.csv&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">mean_ttr</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s2">&#34;time_to_resolve_hours&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">mean</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">std_ttr</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s2">&#34;time_to_resolve_hours&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">std</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">n</span> <span class="o">=</span> <span class="n">df</span><span class="o">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">confidence_interval</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nb">round</span><span class="p">(</span><span class="n">mean_ttr</span> <span class="o">-</span> <span class="mf">1.96</span> <span class="o">*</span> <span class="p">(</span><span class="n">std_ttr</span> <span class="o">/</span> <span class="p">(</span><span class="n">n</span><span class="o">**</span><span class="mf">0.5</span><span class="p">))),</span>
</span></span><span class="line"><span class="cl">    <span class="nb">round</span><span class="p">(</span><span class="n">mean_ttr</span> <span class="o">+</span> <span class="mf">1.96</span> <span class="o">*</span> <span class="p">(</span><span class="n">std_ttr</span> <span class="o">/</span> <span class="p">(</span><span class="n">n</span><span class="o">**</span><span class="mf">0.5</span><span class="p">))),</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Number of tickets: </span><span class="si">{</span><span class="n">n</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Mean time to resolve a ticket: </span><span class="si">{</span><span class="nb">round</span><span class="p">(</span><span class="n">mean_ttr</span><span class="p">)</span><span class="si">}</span><span class="s2"> hours&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Confidence interval: </span><span class="si">{</span><span class="n">confidence_interval</span><span class="si">}</span><span class="s2"> hours&#34;</span><span class="p">)</span>
</span></span></code></pre></div><pre tabindex="0"><code>$ python confidence.py
Number of tickets: 2737
Mean time to resolve a ticket: 11678 hours
Confidence interval: (11075, 12281) hours
</code></pre><p>So we can say to our imaginary PM that &ldquo;based on our historical data, we estimate that the bug will be resolved between 461 days and 511 days from now with 95% confidence.</p>
<p>That&rsquo;s&hellip; not great for our users! Lets dig in a bit deeper based on the <code>Resolution</code> field:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="s2">&#34;Resolution&#34;</span><span class="p">,</span><span class="w"> </span><span class="k">Count</span><span class="p">(</span><span class="s2">&#34;Issue_id&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="s2">&#34;mozilla_firefox.csv&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="s2">&#34;Status&#34;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;RESOLVED&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">AND</span><span class="w"> </span><span class="s2">&#34;Component&#34;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;Tabbed Browser&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="k">DESC</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><div class="monotable">
<table>
  <thead>
      <tr>
          <th>Resolution</th>
          <th style="text-align: right">count(Issue_id)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DUPLICATE</td>
          <td style="text-align: right">1867</td>
      </tr>
      <tr>
          <td>WORKSFORME</td>
          <td style="text-align: right">754</td>
      </tr>
      <tr>
          <td>FIXED</td>
          <td style="text-align: right">585</td>
      </tr>
      <tr>
          <td>INCOMPLETE</td>
          <td style="text-align: right">535</td>
      </tr>
      <tr>
          <td>INVALID</td>
          <td style="text-align: right">530</td>
      </tr>
      <tr>
          <td>WONTFIX</td>
          <td style="text-align: right">264</td>
      </tr>
      <tr>
          <td>EXPIRED</td>
          <td style="text-align: right">69</td>
      </tr>
  </tbody>
</table>
</div>
<p>Alright, lets just focus on <code>FIXED</code> issues, re-running the export query appropriately:</p>
<pre tabindex="0"><code>$ python confidence.py
Number of tickets: 585
Mean time to resolve a ticket: 9537 hours
Confidence interval: (8290, 10784) hours
</code></pre><p>Welp, sorry Firefox users who had issues with tabs during the period this dataset covers.</p>
<h4 id="lets-do-simulations">Lets Do Simulations</h4>
<p>As easy as this was, for some datasets it is better to run a <em>simulation</em>. In this example, we&rsquo;re going to use the <a href="https://en.wikipedia.org/wiki/Monte_Carlo_method">Monte Carlo simulation</a>. We&rsquo;re going to use 10K iterations, and plot a graph to show the p10 and p90, showing that 80% of simulations falls between the two values:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars</span> <span class="k">as</span> <span class="nn">pl</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="nn">plt</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">pl</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s2">&#34;tabbed_browser_ttr.csv&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">n_iter</span> <span class="o">=</span> <span class="mi">10000</span>
</span></span><span class="line"><span class="cl"><span class="n">n</span> <span class="o">=</span> <span class="n">df</span><span class="o">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">ttr</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s2">&#34;time_to_resolve_hours&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">drop_nulls</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">simulated_means</span> <span class="o">=</span> <span class="p">[</span><span class="n">ttr</span><span class="o">.</span><span class="n">sample</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">with_replacement</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span><span class="o">.</span><span class="n">mean</span><span class="p">()</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n_iter</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">simulated_means_df</span> <span class="o">=</span> <span class="n">pl</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">simulated_means</span><span class="p">,</span> <span class="n">schema</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;mean_ttr&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">p10</span> <span class="o">=</span> <span class="n">simulated_means_df</span><span class="o">.</span><span class="n">quantile</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span><span class="o">.</span><span class="n">get_columns</span><span class="p">()[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">p90</span> <span class="o">=</span> <span class="n">simulated_means_df</span><span class="o">.</span><span class="n">quantile</span><span class="p">(</span><span class="mf">0.9</span><span class="p">)</span><span class="o">.</span><span class="n">get_columns</span><span class="p">()[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">bg</span> <span class="o">=</span> <span class="s2">&#34;#f9ffff&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">fg</span> <span class="o">=</span> <span class="s2">&#34;#33321a&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">plt</span><span class="o">.</span><span class="n">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">6</span><span class="p">),</span> <span class="n">facecolor</span><span class="o">=</span><span class="n">bg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ax</span> <span class="o">=</span> <span class="n">plt</span><span class="o">.</span><span class="n">axes</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">ax</span><span class="o">.</span><span class="n">set_facecolor</span><span class="p">(</span><span class="n">bg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ax</span><span class="o">.</span><span class="n">hist</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">simulated_means_df</span><span class="p">[</span><span class="s2">&#34;mean_ttr&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="n">bins</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">alpha</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">fill</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">color</span><span class="o">=</span><span class="n">fg</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">align</span><span class="o">=</span><span class="s2">&#34;mid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">width</span><span class="o">=</span><span class="mi">75</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ax</span><span class="o">.</span><span class="n">axvline</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">x</span><span class="o">=</span><span class="n">p10</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">color</span><span class="o">=</span><span class="n">fg</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">linestyle</span><span class="o">=</span><span class="s2">&#34;dotted&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">label</span><span class="o">=</span><span class="s2">&#34;10th Percentile (~8743 hours)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ax</span><span class="o">.</span><span class="n">axvline</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">x</span><span class="o">=</span><span class="n">p90</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">color</span><span class="o">=</span><span class="n">fg</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">linestyle</span><span class="o">=</span><span class="s2">&#34;dashed&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">label</span><span class="o">=</span><span class="s2">&#34;90th Percentile (~10370 hours)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ax</span><span class="o">.</span><span class="n">tick_params</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="s2">&#34;both&#34;</span><span class="p">,</span> <span class="n">colors</span><span class="o">=</span><span class="n">fg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="n">ax</span><span class="o">.</span><span class="n">spines</span><span class="p">[</span><span class="n">spine</span><span class="p">]</span><span class="o">.</span><span class="n">set_color</span><span class="p">(</span><span class="n">fg</span><span class="p">)</span> <span class="k">for</span> <span class="n">spine</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">&#34;left&#34;</span><span class="p">,</span> <span class="s2">&#34;bottom&#34;</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="n">ax</span><span class="o">.</span><span class="n">spines</span><span class="p">[</span><span class="n">spine</span><span class="p">]</span><span class="o">.</span><span class="n">set_color</span><span class="p">(</span><span class="n">bg</span><span class="p">)</span> <span class="k">for</span> <span class="n">spine</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">&#34;right&#34;</span><span class="p">,</span> <span class="s2">&#34;top&#34;</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl"><span class="n">plt</span><span class="o">.</span><span class="n">title</span><span class="p">(</span><span class="s2">&#34;Distribution of Simulated Mean Resolution Times&#34;</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">12</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">fg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">plt</span><span class="o">.</span><span class="n">xlabel</span><span class="p">(</span><span class="s2">&#34;Mean Time to Resolve Issues (hours)&#34;</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">fg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">plt</span><span class="o">.</span><span class="n">ylabel</span><span class="p">(</span><span class="s2">&#34;Frequency&#34;</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">fg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">plt</span><span class="o">.</span><span class="n">legend</span><span class="p">(</span><span class="n">labelcolor</span><span class="o">=</span><span class="n">fg</span><span class="p">,</span> <span class="n">shadow</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">frameon</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">plt</span><span class="o">.</span><span class="n">tight_layout</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">plt</span><span class="o">.</span><span class="n">savefig</span><span class="p">(</span><span class="s2">&#34;monte_carlo.webp&#34;</span><span class="p">)</span>
</span></span></code></pre></div><figure><img src="/leadership-power-tools-sql-and-statistics/monte_carlo.webp"
    alt="A graph showing the distribution of means from the Monte Carlo simulation. The 10th and 90th percentile are marked at 8743 hours and 10370 hours respectively.">
</figure>

<p>(Honestly, <code>matplotlib</code> is a power tool in itself - very much recommend learning how to plot your common distributions with it.)</p>
<p>Here, we can say that 80% of the simulations landed between 8743 hours and 10370 hours - a slightly tighter interval than our pure maths one above, but still pretty broad.</p>
<p>This method I find particularly useful in capacity estimation, though this relies on a relatively stable team to be useful. As the team changes, you have to throw out old data - past performance does not indicate guarantee future results, after all.</p>
<p>You can also apply Monte Carlo simulations to other useful domains: incident response, budgeting and estimating defect rates during the lifetime of a project.</p>
<h3 id="bayesian-reasoning">Bayesian Reasoning</h3>
<p>As engineering leaders, we must strive to be &ldquo;good Bayesians&rdquo; - that is, <a href="https://en.wikipedia.org/wiki/Bayesian_inference">we update our beliefs about the world/processes/system/people as new data becomes available</a>.</p>
<p>Frankly, getting into this in a serious level of detail is beyond the scope of this post. However, we can give a pretty close-to-home example at a high level - project estimation.</p>
<h4 id="estimating-probability-of-project-completion">Estimating Probability of Project Completion</h4>
<p>We need to start with an initial belief. Imagine we had a dataset similar to the above with all our previously completed projects, with estimated start and end dates, and actual start and end dates. From the data, the team completes projects within the initial estimate 70% of the time, so the probability of not completing on time is 0.3</p>
<p>We&rsquo;re part way through our project, and we&rsquo;ve completed 40% of the work so far. From our historical data, if we&rsquo;re on time, normally 50% ± 10% of the work is complete by this point. If we&rsquo;re off track, its 30% ± 10% of the work completed.</p>
<p>We now need to apply <a href="https://en.wikipedia.org/wiki/Bayes%27_theorem">Bayes&rsquo; theorem</a>:</p>
<pre tabindex="0"><code>P(H/D) = (P(D|H) * P(H) / P(D))
</code></pre><p><code>P(H/D)</code> is our <strong>posterior probability</strong>, <code>P(D|H)</code> is our <strong>likelihood</strong>, <code>P(H)</code> is our <strong>prior probability</strong> and <code>P(D)</code> is our <strong>evidence</strong>.</p>
<p>We can now calculate each component. If we assume a normal distribution of probabilities (like from the Monte Carlo simulation above), our likelihood value for &ldquo;still on time&rdquo; is 0.24. Conversely, for &ldquo;not on time&rdquo;, we get 0.13.</p>
<p>Our evidence is a normalising constant, which evaluates to 0.207.</p>
<p>From there we can plug our figures into Bayes&rsquo; theorem:</p>
<p>For <strong>on time</strong>, we get <code>(0.24 * 0.7) / 0.207 = 0.8115952</code>, or ~81% probability.<br>
For <strong>off time</strong>, we get <code>(0.13 * 0.3) / 0.207 = 0.1884057</code>, or ~19% probability.</p>
<p>When we complete another project, we can update our model. Again, this ties into providing high quality estimates to stakeholders.</p>
<h2 id="wrap-up">Wrap Up</h2>
<p>We&rsquo;ve covered getting access to the data that is important to you, using tools like DuckDB and Python to analyse it, and apply statistical methods to understand the data and speak precisely about what the data means for you and your decision making. However, with great power comes great responsibility - always double check your work and ensure it aligns with the business goals.</p>
<p>If you&rsquo;d like to get even more into stats (and why wouldn&rsquo;t you!) consider <a href="https://minireference.com/blog/fixing-the-statistics-curriculum/#plan">this post by Ivan Savov</a> on a a revised statistics curriculum. We&rsquo;ve covered, briefly, probability distributions, confidence intervals and Bayesian reasoning (briefly), but it is worth looking into linear regression. I also recommend <a href="https://greenteapress.com/thinkstats2/html/index.html">Think Stats</a> and <a href="https://minireference.com/blog/python-for-stats/">Python for Stats</a>.</p>]]></content:encoded></item><item><title>7 Databases in 7 Weeks for 2025</title><link>https://matt.blwt.io/post/7-databases-in-7-weeks-for-2025/</link><pubDate>Sat, 30 Nov 2024 17:39:22 +0000</pubDate><guid>https://matt.blwt.io/post/7-databases-in-7-weeks-for-2025/</guid><description>&lt;p&gt;I&amp;rsquo;ve been running databases-as-a-service for a long time, and there are always new things to keep abreast of - new technologies, different ways of solving problems, not to mention all the research coming out of universities. In 2025, consider spending a week with each of these database technologies.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>I&rsquo;ve been running databases-as-a-service for a long time, and there are always new things to keep abreast of - new technologies, different ways of solving problems, not to mention all the research coming out of universities. In 2025, consider spending a week with each of these database technologies.</p>

<figure >
    
        <img src="/7-databases-in-7-weeks-for-2025/header.webp" alt="A line drawing of a bookshelf, with the books labelled for each database covered - PostgreSQL, SQLite, DuckDB, ClickHouse, FoundationDB, TigerBeetle and CockroachDB" />
    
    
</figure>


<h2 id="preamble">Preamble</h2>
<p>These aren&rsquo;t the &ldquo;7 Best Databases&rdquo; or something similar to power a Buzzfeed listicle - these are just 7 databases that I think are worth your time to really look into for a week or so. You might ask something like &ldquo;why not Neo4j or MongoDB or MySQL/Vitess or &lt;insert other db here&gt;&rdquo; - the answer is mostly that I don&rsquo;t find them interesting. I&rsquo;m also not covering Kafka or other similar streaming data services - definitely worth your time, but not covered.</p>
<p>This post was inspired by the original book <a href="https://7dbs.io/">7 Databases In 7 Weeks</a> by Luc Perkins with Eric Redmond and Jim Wilson.</p>
<h2 id="table-of-contents">Table of Contents</h2>
<ol>
<li><a href="#1-postgresql">PostgreSQL</a></li>
<li><a href="#2-sqlite">SQLite</a></li>
<li><a href="#3-duckdb">DuckDB</a></li>
<li><a href="#4-clickhouse">ClickHouse</a></li>
<li><a href="#5-foundationdb">FoundationDB</a></li>
<li><a href="#6-tigerbeetle">TigerBeetle</a></li>
<li><a href="#7-cockroachdb">CockroachDB</a></li>
</ol>
<ul>
<li><a href="#wrap-up">Wrap Up</a></li>
</ul>
<h2 id="1-postgresql">1. PostgreSQL</h2>
<h3 id="the-default-database">The Default Database</h3>
<p>&ldquo;Just use Postgres&rdquo; is basically a meme at this point, and for good reason. <a href="https://www.postgresql.org/">PostgreSQL</a> is the pinnacle of <a href="https://boringtechnology.club/">boring technology</a>, and should be the database you reach for when you need a client-server model for your database. ACID compliant, plenty of interesting tricks for replication - both physical and logical - and incredibly well supported across all the major vendors.</p>
<p>My favourite feature of Postgres, however, are <a href="https://wiki.postgresql.org/wiki/Extensions">extensions</a>. This is where I feel Postgres really comes alive in a way that few other databases can. There are extensions for almost everything you could want - <a href="https://age.apache.org/">AGE</a> enables graph data structures and the user of the Cypher query language, <a href="https://docs.timescale.com/self-hosted/latest/">TimescaleDB</a> enables time-series workloads, <a href="https://github.com/hydradatabase/hydra/tree/main/columnar">Hydra Columnar</a> provides an alternate columnar storage engine, and so on. I&rsquo;ve <a href="/post/building-a-postgresql-extension-line-by-line">written about writing an extension</a> relatively recently if you&rsquo;d like to give it a go yourself.</p>
<p>Postgres shines as a great &ldquo;default&rdquo; database for that reason, and we&rsquo;re seeing even more non-Postgres services rely on the <a href="https://www.postgresql.org/docs/current/protocol.html">Postgres wire protocol</a> as a general-purpose Layer 7 protocol to provide client compatibility. With a rich ecosystem, sensible default behaviour and that it can even be fit into a <a href="https://pglite.dev/">Wasm install</a> makes it a database worth understanding.</p>
<p>Spend a week learning about whats possible with Postgres, but also some of its limitations - <a href="https://www.geeksforgeeks.org/multiversion-concurrency-control-mvcc-in-postgresql/">MVCC</a> can be fickle. Implement a simple CRUD app in your favourite language. Maybe even build a Postgres extension.</p>
<h2 id="2-sqlite">2. SQLite</h2>
<h3 id="the-local-first-database">The Local-First Database</h3>
<p>Moving on from a client-server model, we take a detour into &ldquo;embedded&rdquo; databases, starting with <a href="https://www.sqlite.org/index.html">SQLite</a>. I&rsquo;ve termed this the &ldquo;<a href="https://www.inkandswitch.com/local-first/">local-first</a>&rdquo; database, where the SQLite database is directly co-located with the application. One of the more famous examples of this usage is <a href="https://www.whatsapp.com/">WhatsApp</a>, which stored chats as local SQLite databases on the device being used. <a href="https://signal.org/">Signal</a> also does the same thing.</p>
<p>Beyond that, we&rsquo;re starting to see more creative uses of SQLite rather than &ldquo;just&rdquo; a local ACID-compliant database. With the advent of tools like <a href="https://litestream.io/">Litestream</a> enabling streaming backups and <a href="https://fly.io/docs/litefs/">LiteFS</a> to provide distributed access, we can devise more interesting topologies. Extensions like <a href="https://github.com/vlcn-io/cr-sqlite">CR-SQLite</a> allow the use of <a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">CRDTs</a> to avoid needing conflict resolution when merging changesets, as used in <a href="https://github.com/superfly/corrosion">Corrosion</a>.</p>
<p>SQLite has also had a small resurgence thanks to <a href="https://rubyonrails.org/2024/9/27/rails-8-beta1-no-paas-required">Ruby on Rails 8.0</a> - 37signals has gone all in on SQLite, building a bunch of Rails modules like <a href="https://github.com/rails/solid_queue">Solid Queue</a> and configuring Rails to manipulate multiple SQLite databases via <code>database.yml</code> for this purpose. <a href="https://newsletter.pragmaticengineer.com/p/bluesky?open=false#%C2%A7sqlite">Bluesky uses SQLite for the Personal Data Servers</a> - every user has their own SQLite database.</p>
<p>Spend a week experimenting with local-first architectures using SQLite, or even seeing if you can migrate a client-server model using Postgres to something that &ldquo;just&rdquo; needs SQLite instead.</p>
<h2 id="3-duckdb">3. DuckDB</h2>
<h3 id="the-query-anything-database">The Query-Anything Database</h3>
<p>Onto the next embedded database, we have <a href="https://duckdb.org/">DuckDB</a>. Much like SQLite, DuckDB is intended to be an in-process database system, but more focused on online analytical processing (OLAP) versus online transaction processing (OLTP).</p>
<p>Where DuckDB shines is its use as a &ldquo;query-anything&rdquo; database, using SQL as its dialect of choice. It can natively pull data into its engine from CSVs, TSVs, JSON etc, but also formats like Parquet - just check out the list of <a href="https://duckdb.org/docs/data/data_sources.html">data sources</a>. This gives it extreme flexibility - check out <a href="https://motherduck.com/blog/how-to-extract-analytics-from-bluesky/">this example of querying the Bluesky firehose</a>.</p>
<p>Much like Postgres, DuckDB also <a href="https://duckdb.org/docs/extensions/overview">has extensions</a>, though not quite as rich an ecosystem - DuckDB is much younger, after all. Many contributed by the community can be found on the <a href="https://duckdb.org/community_extensions/list_of_extensions">list of community extensions</a>, though a particular favourite of mine is <a href="https://duckdb.org/community_extensions/extensions/gsheets.html"><code>gsheets</code></a>.</p>
<p>Spend a week doing some data analysis and processing with DuckDB - be it via a Python notebook or something like <a href="https://evidence.dev/">Evidence</a>, maybe even see how it fits in with your &ldquo;local-first&rdquo; approach with SQLite by offloading analytics queries of your SQLite database to DuckDB, which <a href="https://duckdb.org/docs/guides/database_integration/sqlite.html">can read it</a>.</p>
<h2 id="4-clickhouse">4. ClickHouse</h2>
<h3 id="the-columnar-database">The Columnar Database</h3>
<p>Leaving the embedded database sphere, but sticking with the analytics theme, we come to <a href="https://clickhouse.com/">ClickHouse</a>. If I had to only pick two databases to deal with, I&rsquo;d be quite happy with just Postgres and ClickHouse - the former for OLTP, the latter for OLAP.</p>
<p>ClickHouse specialises in analytics workloads, and can support very high ingest rates through <a href="https://clickhouse.com/docs/en/architecture/horizontal-scaling">horizontal scaling</a> and sharded storage. It also supports <a href="https://clickhouse.com/docs/en/guides/separation-storage-compute">tiered storage</a>, allowing you to split &ldquo;hot&rdquo; and &ldquo;cold&rdquo; data - <a href="https://docs.gitlab.com/ee/development/database/clickhouse/tiered_storage.html">GitLab</a> have a pretty thorough doc on this.</p>
<p>Where ClickHouse comes into its own is when you have analytics queries to run on a dataset too big for something like DuckDB, or you need &ldquo;real-time&rdquo; analytics. There is a lot of &ldquo;benchmarketing&rdquo; around these datasets, so I&rsquo;m not going to repeat them here.</p>
<p>Another reason I suggest checking out ClickHouse is that it is a <em>joy</em> to operate - deployment, scaling, backups and so on are <a href="https://clickhouse.com/docs/en/architecture/cluster-deployment">well documented</a> - even down to setting <a href="https://clickhouse.com/docs/en/operations/tips">the right CPU governor</a> is covered.</p>
<p>Spend a week exploring some larger analytics datasets, or converting some of the DuckDB analytics from above into a ClickHouse deployment. ClickHouse also has an embedded version - <a href="https://clickhouse.com/docs/en/chdb">chDB</a> - that can offer a more direct comparison.</p>
<h2 id="5-foundationdb">5. FoundationDB</h2>
<h3 id="the-layered-database">The Layered Database</h3>
<p>We now enter the &ldquo;mind expanding&rdquo; section of this list, with <a href="https://www.foundationdb.org/">FoundationDB</a>. Arguably, FoundationDB is not a database, but quite literally the foundation for <em>a</em> database. Used in production by Apple, Snowflake and <a href="https://www.tigrisdata.com/blog/building-a-database-using-foundationdb/">Tigris Data</a>, FoundationDB is worth your time because it is quite unique in the world of key-value storage.</p>
<p>Yes, it&rsquo;s an ordered key-value store, but that isn&rsquo;t what is interesting about it. At first glance, it has some curious <a href="https://apple.github.io/foundationdb/known-limitations.html">limitations</a> - transactions cannot exceed 10MB of affected data and they cannot take longer than five seconds after the first read in a transaction. But, as they say, limits set us free. By having these limits, it can achieve full ACID transactions at very large scale - 100+ TiB clusters are known to be in operation.</p>
<p>FoundationDB is architected for specific workloads and <a href="https://apple.github.io/foundationdb/testing.html">extensively tested</a> using simulation testing, which has been picked up by other technologies, including another database on this list and <a href="https://www.antithesis.com/">Antithesis</a>, founded by some ex-FoundationDB folks. For more notes on this, check out <a href="https://sled.rs/simulation.html">Tyler Neely&rsquo;s</a> and <a href="https://notes.eatonphil.com/2024-08-20-deterministic-simulation-testing.html">Phil Eaton&rsquo;s</a> notes on the topic.</p>
<p>As mentioned, FoundationDB has some very specific semantics that take some getting used to - their <a href="https://apple.github.io/foundationdb/anti-features.html">Anti-Features</a> and <a href="https://apple.github.io/foundationdb/features.html">Features</a> docs are worth familiarising yourself with to understand the problems they are looking to solve.</p>
<p>But why is it the &ldquo;layered&rdquo; database? This is because of the <a href="https://apple.github.io/foundationdb/layer-concept.html">Layers concept</a>. Instead of tying the storage engine to the data model, instead the storage is flexible enough to be remapped across different layers. <a href="https://www.tigrisdata.com/blog/data-layer-foundationdb/">Tigris Data</a> have a great post about building such a layer, and there are some examples such as a <a href="https://github.com/FoundationDB/fdb-record-layer">Record layer</a> and a <a href="https://github.com/FoundationDB/fdb-document-layer">Document layer</a> from the FoundationDB org.</p>
<p>Spend a week going through the <a href="https://apple.github.io/foundationdb/tutorials.html">tutorials</a> and think about how you could use FoundationDB in place of something like <a href="https://rocksdb.org/">RocksDB</a>. Maybe check out some of the <a href="https://apple.github.io/foundationdb/design-recipes.html">Design Recipes</a> and go read the <a href="https://www.foundationdb.org/files/fdb-paper.pdf">paper</a>.</p>
<h2 id="6-tigerbeetle">6. TigerBeetle</h2>
<h3 id="the-obsessively-correct-database">The Obsessively Correct Database</h3>
<p>Flowing on from the deterministic simulation testing, <a href="https://tigerbeetle.com/">TigerBeetle</a> breaks the mold from our previous databases in that it is decidedly <em>not</em> a general purpose database - it is entirely dedicated to financial transactions.</p>
<p>Why is this worth a look? Single-purpose databases are unusual, and one that is as <em>obsessively correct</em> as TigerBeetle are a true rarity, especially considering it is open source. They include everything from <a href="https://en.wikipedia.org/wiki/The_Power_of_10:_Rules_for_Developing_Safety-Critical_Code">NASA&rsquo;s Power of Ten Rules</a> and <a href="https://www.usenix.org/conference/fast18/presentation/alagappan">Protocol-Aware Recovery</a>, through to strict serialisability and Direct I/O to avoid issues with the kernel page cache. It is <em>seriously</em> impressive - just go read their <a href="https://github.com/tigerbeetle/tigerbeetle/blob/a43f2205f5335cb8f56d6e8bfcc6b2d99a4fc4a4/docs/about/safety.md">Safety doc</a> and their <a href="https://github.com/tigerbeetle/tigerbeetle/blob/a43f2205f5335cb8f56d6e8bfcc6b2d99a4fc4a4/docs/TIGER_STYLE.md">approach to programming they call Tiger Style</a>.</p>
<p>Another interesting point about TigerBeetle is that it&rsquo;s written in <a href="https://ziglang.org/">Zig</a> - a relative newcomer to the systems programming language school, but clearly has fit well with what the TigerBeetle folks are trying to accomplish.</p>
<p>Spend a week modelling your financial accounts in a local deployment of TigerBeetle - follow the <a href="https://docs.tigerbeetle.com/quick-start">Quick Start</a> and take a look at the <a href="https://docs.tigerbeetle.com/coding/system-architecture">System Architecture</a> docs on how you might use it in conjunction with one of the more general-purpose databases above.</p>
<h2 id="7-cockroachdb">7. CockroachDB</h2>
<h3 id="the-global-database">The Global Database</h3>
<p>Finally, we come full circle. I struggled a little on what to put here in the last slot. Thoughts originally went to <a href="https://valkey.io/">Valkey</a>, but FoundationDB scratched the key-value itch. I thought about graph databases, or something like <a href="https://www.scylladb.com/">ScyllaDB</a> or <a href="https://cassandra.apache.org/_/index.html">Cassandra</a>. I thought about <a href="https://aws.amazon.com/dynamodb/">DynamoDB</a>, but not being able to run it locally/for free put me off.</p>
<p>In the end, I decided to close on a globally distributed database - <a href="https://www.cockroachlabs.com/">CockroachDB</a>. It&rsquo;s Postgres wire-protocol compatible, and inherits some of the more interesting features discussed above - large horizontal scaling, strong consistency - and has some interesting features of its own.</p>
<p>CockroachDB enables scaling a database across multiple geographies through being based on Google&rsquo;s <a href="http://static.googleusercontent.com/media/research.google.com/en//archive/spanner-osdi2012.pdf">Spanner</a> system, which relies on atomic and GPS clocks for extremely accurate time synchronisation. Commodity hardware, however, doesn&rsquo;t have such luxuries, so CockroachDB has some <a href="https://www.cockroachlabs.com/blog/living-without-atomic-clocks/#How-does-CockroachDB-choose-transaction-timestamps?">clever solutions</a> where reads are retried or delayed to account for clock sync delay with NTP, and nodes also compare clock drift amongst themselves and terminate members if they exceed the maximum offset.</p>
<p>Another interesting feature of CockroachDB is how <a href="https://www.cockroachlabs.com/docs/stable/multiregion-overview">multi-region configurations</a> are used, including <a href="https://www.cockroachlabs.com/docs/stable/table-localities">table localities</a>, where there are different options depending on the read/write tradeoffs you want to make.</p>
<p>Spend a week re-implementing the <a href="https://www.cockroachlabs.com/docs/v24.3/movr"><code>movr</code></a> example in a language and framework of your choice.</p>
<h2 id="wrap-up">Wrap Up</h2>
<p>We&rsquo;ve explored a bunch of different databases, all used in production by some of the largest companies on the planet, and hopefully this will have exposed you to some technologies you weren&rsquo;t familiar with before. Take this knowledge with you as you look to solve interesting problems.</p>]]></content:encoded></item><item><title>7 Languages in 7 Weeks for 2025</title><link>https://matt.blwt.io/post/7-languages-in-7-weeks-for-2025/</link><pubDate>Mon, 25 Nov 2024 06:00:00 +0000</pubDate><guid>https://matt.blwt.io/post/7-languages-in-7-weeks-for-2025/</guid><description>&lt;p&gt;It&amp;rsquo;s been over 14 years since the original &lt;a href="https://pragprog.com/titles/btlang/seven-languages-in-seven-weeks/"&gt;&lt;em&gt;7 Languages in 7 Weeks&lt;/em&gt;&lt;/a&gt; was first published, giving a hands on tour of Ruby, Clojure, Haskell, Io, Scala, Erlang and Prolog. Ruby achieved critical mass, to some degree so did Scala, with the others being popular within their specific niches. This post shows 7 languages worth exploring in 2025.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>It&rsquo;s been over 14 years since the original <a href="https://pragprog.com/titles/btlang/seven-languages-in-seven-weeks/"><em>7 Languages in 7 Weeks</em></a> was first published, giving a hands on tour of Ruby, Clojure, Haskell, Io, Scala, Erlang and Prolog. Ruby achieved critical mass, to some degree so did Scala, with the others being popular within their specific niches. This post shows 7 languages worth exploring in 2025.</p>

<figure >
    
        <img src="/7-languages-in-7-weeks-for-2025/header.webp" alt="A line-drawing of the number 7, in old Macintosh colours" />
    
    
</figure>


<p>The 7 languages we&rsquo;re going to look at are, notably, ones that aren&rsquo;t overlapping with <a href="https://pragprog.com/titles/dzseven/seven-obscure-languages-in-seven-weeks/"><em>7 Obscure Languages in 7 Weeks</em></a>. These are going to be a bit more mainstream. I&rsquo;ll go through each language explaining why you should have it in your toolbox, followed by a small demo of what I feel makes the language unique. The first 3 languages are common, the latter 4 much less so.</p>
<h2 id="table-of-contents">Table of Contents</h2>
<ol>
<li><a href="#1-python">Python</a></li>
<li><a href="#2-typescript">TypeScript</a></li>
<li><a href="#3-go">Go</a></li>
<li><a href="#4-gleam">Gleam</a></li>
<li><a href="#5-zig">Zig</a></li>
<li><a href="#6-racket">Racket</a></li>
<li><a href="#7-odin">Odin</a></li>
</ol>
<ul>
<li><a href="#wrap-up">Wrap Up</a></li>
</ul>
<h2 id="1-python">1. Python</h2>
<p>Really, <a href="https://www.python.org/">Python</a> is about as mainstream as it gets - being ranked <a href="https://github.blog/news-insights/octoverse/octoverse-2024/">#1 in GitHub&rsquo;s Octoverse report for 2024</a> and <a href="https://survey.stackoverflow.co/2024/technology#most-popular-technologies-language">#3 in Stack Overflow&rsquo;s survey for 2024</a>. So why include this at all?</p>
<p>The reason is twofold - interoperability with the hottest technology of the last decade, and Python notebooks (with associated statistics packages). The latter, in my opinion, is Python&rsquo;s USP - competing against R and Julia, but with significantly more ubiquity and flexibility than the former two.</p>
<p>To give an example of the latter - there are <em>plenty</em> of tutorials on machine learning models and integrating LLMs if you search around - lets leverage <a href="https://duckdb.org/docs/api/python/overview.html">DuckDB</a> and <code>matplotlib</code> to quickly generate a graph using notebooks in <a href="https://code.visualstudio.com/docs/datascience/jupyter-notebooks">VSCode</a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">duckdb</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="nn">plt</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">seaborn</span> <span class="k">as</span> <span class="nn">sns</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">%</span><span class="n">matplotlib</span> <span class="n">inline</span>
</span></span><span class="line"><span class="cl"><span class="o">%</span><span class="n">config</span> <span class="n">inlineBackend</span><span class="o">.</span><span class="n">figure_format</span> <span class="o">=</span> <span class="s1">&#39;retina&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># inline type hints!</span>
</span></span><span class="line"><span class="cl"><span class="n">GRAPHRC</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">tuple</span> <span class="o">|</span> <span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;figure.figsize&#34;</span><span class="p">:</span> <span class="p">(</span><span class="mf">11.7</span><span class="p">,</span> <span class="mf">8.27</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;figure.facecolor&#34;</span><span class="p">:</span> <span class="s2">&#34;#f9ffff&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;axes.facecolor&#34;</span><span class="p">:</span> <span class="s2">&#34;#f9ffff&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">GRAPHSTYLE</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&#34;dark&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">GRAPHFONT</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&#34;sans-serif&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sns</span><span class="o">.</span><span class="n">set_theme</span><span class="p">(</span><span class="n">style</span><span class="o">=</span><span class="n">GRAPHSTYLE</span><span class="p">,</span> <span class="n">rc</span><span class="o">=</span><span class="n">GRAPHRC</span><span class="p">,</span> <span class="n">font</span><span class="o">=</span><span class="n">GRAPHFONT</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">db</span> <span class="o">=</span> <span class="n">duckdb</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s2">&#34;title.ratings.tsv.gz&#34;</span><span class="p">,</span> <span class="n">sep</span><span class="o">=</span><span class="s2">&#34;</span><span class="se">\t</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">query</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">SELECT
</span></span></span><span class="line"><span class="cl"><span class="s2">    tconst,
</span></span></span><span class="line"><span class="cl"><span class="s2">    averageRating
</span></span></span><span class="line"><span class="cl"><span class="s2">FROM
</span></span></span><span class="line"><span class="cl"><span class="s2">    ratings
</span></span></span><span class="line"><span class="cl"><span class="s2">WHERE
</span></span></span><span class="line"><span class="cl"><span class="s2">    numVotes &gt; 50
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="s2">&#34;ratings&#34;</span><span class="p">,</span> <span class="n">query</span><span class="p">)</span><span class="o">.</span><span class="n">pl</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ax</span> <span class="o">=</span> <span class="n">plt</span><span class="o">.</span><span class="n">axes</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">ax</span><span class="o">.</span><span class="n">xaxis</span><span class="o">.</span><span class="n">label</span><span class="o">.</span><span class="n">set_color</span><span class="p">(</span><span class="s2">&#34;#33321a&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ax</span><span class="o">.</span><span class="n">yaxis</span><span class="o">.</span><span class="n">label</span><span class="o">.</span><span class="n">set_color</span><span class="p">(</span><span class="s2">&#34;#33321a&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ax</span><span class="o">.</span><span class="n">tick_params</span><span class="p">(</span><span class="n">which</span><span class="o">=</span><span class="s2">&#34;both&#34;</span><span class="p">,</span> <span class="n">colors</span><span class="o">=</span><span class="s2">&#34;#33321a&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ax</span><span class="o">.</span><span class="n">title</span><span class="o">.</span><span class="n">set_color</span><span class="p">(</span><span class="s2">&#34;#33321a&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ax</span><span class="o">.</span><span class="n">margins</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">pt</span> <span class="o">=</span> <span class="n">sns</span><span class="o">.</span><span class="n">histplot</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">df</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">x</span><span class="o">=</span><span class="s2">&#34;averageRating&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">bins</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">kde</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">color</span><span class="o">=</span><span class="s2">&#34;#33321a&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">ax</span><span class="o">=</span><span class="n">ax</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">plt</span><span class="o">.</span><span class="n">tight_layout</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">plt</span><span class="o">.</span><span class="n">title</span><span class="p">(</span><span class="s2">&#34;IMDb Ratings&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">plt</span><span class="o">.</span><span class="n">xlabel</span><span class="p">(</span><span class="s2">&#34;Rating&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">plt</span><span class="o">.</span><span class="n">ylabel</span><span class="p">(</span><span class="s2">&#34;Count&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">plt</span><span class="o">.</span><span class="n">show</span><span class="p">()</span>
</span></span></code></pre></div>
<figure >
    
        <img src="/7-languages-in-7-weeks-for-2025/graph.webp" alt="A graph showing a histogram of IMDB ratings" />
    
    
</figure>


<p>A handful of code and we&rsquo;ve got a histogram plotted out with a kernel density estimate, with our filtering done via SQL and reading straight from the gzipped TSV from <a href="https://datasets.imdbws.com/">IMDb Non-Commercial Datasets</a>. We can go even further with Simon Willison&rsquo;s wonderful <a href="https://datasette.io/">Datasette</a> for exploratory data analysis and presentation. Wow your colleagues and bosses with quick and dirty notebooks. Spend a week getting familiar with these integrations and keep it in your back pocket the next time you reach for Google Sheets or Excel.</p>
<h2 id="2-typescript">2. TypeScript.</h2>
<p><a href="https://www.typescriptlang.org/">TypeScript</a> gives us typed JavaScript. Why should you care? Perhaps best summed up in this meme from <a href="https://bsky.app/profile/anniesexton.com">Annie Sexton</a>:</p>

<figure >
    
        <img src="/7-languages-in-7-weeks-for-2025/mytypes.webp" alt="A dithered two-colour image of Velma, from Scooby Doo, crawling around on the floor without her glasses saying &#39;My types! I can&#39;t see without my types!&#39;" />
    
    
</figure>


<p>Typed languages are, fundamentally, safer languages to write in - being able to declare expected types and enforce their usage lets you have increased confidence that the software you write is correct.</p>
<p>What I find unique about TypeScript is its approach to gradual typing. If you don&rsquo;t have a greenfield project, slowly introducing types lets you reduce risk piecemeal. Here&rsquo;s an example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">compact</span> <span class="o">=</span> <span class="p">(</span><span class="nx">arr</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">orr</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">10</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">arr</span><span class="p">.</span><span class="nx">trim</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">arr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><p>We&rsquo;ve got a clear error above - <code>orr</code> is not <code>arr</code> and therefore would crash at runtime:</p>
<pre tabindex="0"><code>&gt; compact([1,2,3])
Uncaught ReferenceError: orr is not defined
    at compact (REPL6:2:3)
</code></pre><p>It&rsquo;s also ambiguous what <code>arr</code> is - multiple object instances have a <code>length</code> property, and <code>trim()</code> is not a valid Array method. So, we can gradually type this function, first in our editor:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// @ts-check
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">compact</span> <span class="o">=</span> <span class="p">(</span><span class="nx">arr</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">orr</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">10</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Cannot find name &#39;orr&#39;.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">return</span> <span class="nx">arr</span><span class="p">.</span><span class="nx">trim</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">arr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><p>Then declare our param type via JSDoc comments:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// @ts-check
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"><span class="cm">/** @param {any[]} arr */</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">compact</span> <span class="o">=</span> <span class="p">(</span><span class="nx">arr</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">arr</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">10</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">arr</span><span class="p">.</span><span class="nx">trim</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span> <span class="c1">// Property &#39;trim&#39; does not exist on type &#39;any[]&#39;.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">arr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><p>Then finally convert it over with real type declarations:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">compact</span> <span class="o">=</span> <span class="p">(</span><span class="nx">arr</span>: <span class="kt">string</span><span class="p">[])</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">arr</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">10</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">arr</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">arr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><p>This approach to gradual typing, I feel, is pretty special. Other languages that have introduced typing, either via annotations like Python or separate systems like Ruby, do not pull it off as elegantly as this. Besides, TypeScript is one of the very few &ldquo;universal&rdquo; languages to write a complete application in. Consider spending a week writing a &ldquo;full stack&rdquo; application in TypeScript using <a href="https://nextjs.org/">Next.js</a>.</p>
<h2 id="3-go">3. Go</h2>
<p><a href="https://go.dev/">Go</a> remains probably my personal favourite language to write, and it is worth your time too. So why Go? Its most compelling points are a &ldquo;batteries-included&rdquo; standard library, small surface area to learn (much like my favourite small language <a href="/post/lua-the-little-language-that-could">Lua</a>) and easy binary shipping, including generating static binaries.</p>
<p>However, the &ldquo;killer app&rdquo; for Go is that coupled with a great type system, it is phenomenally easy to write applications that are easy to maintain. The Go team&rsquo;s commitment to iterative improvement to the language without breaking backwards compatibility is extremely commendable.</p>
<p>Rather than providing a direct demo, I&rsquo;m going to take a sample of code from my <a href="https://github.com/mble/redis-rest-api"><code>redis-rest-api</code></a> project:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">newServer</span><span class="p">(</span><span class="nx">ctx</span><span class="w"> </span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span><span class="w"> </span><span class="nx">cfg</span><span class="w"> </span><span class="nx">serverCfg</span><span class="p">,</span><span class="w"> </span><span class="nx">rc</span><span class="w"> </span><span class="o">*</span><span class="nx">redis</span><span class="p">.</span><span class="nx">Client</span><span class="p">,</span><span class="w"> </span><span class="nx">um</span><span class="w"> </span><span class="nx">UserMap</span><span class="p">,</span><span class="w"> </span><span class="nx">auth</span><span class="w"> </span><span class="nx">authFunc</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Server</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="kd">const</span><span class="w"> </span><span class="nx">maxBytes</span><span class="w"> </span><span class="kt">int64</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="mi">1048576</span><span class="w"> </span><span class="c1">// 1 MiB</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">mux</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nf">NewServeMux</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">mux</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span><span class="w"> </span><span class="nf">rootHandler</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">rc</span><span class="p">,</span><span class="w"> </span><span class="nx">um</span><span class="p">,</span><span class="w"> </span><span class="nx">auth</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">mux</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/pipeline&#34;</span><span class="p">,</span><span class="w"> </span><span class="nf">pipelineHandler</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">rc</span><span class="p">,</span><span class="w"> </span><span class="nx">um</span><span class="p">,</span><span class="w"> </span><span class="nx">auth</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">mux</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/multi-exec&#34;</span><span class="p">,</span><span class="w"> </span><span class="nf">txHandler</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span><span class="w"> </span><span class="nx">rc</span><span class="p">,</span><span class="w"> </span><span class="nx">um</span><span class="p">,</span><span class="w"> </span><span class="nx">auth</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">wrappedMux</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">http</span><span class="p">.</span><span class="nf">MaxBytesHandler</span><span class="p">(</span><span class="nx">mux</span><span class="p">,</span><span class="w"> </span><span class="nx">maxBytes</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">server</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">http</span><span class="p">.</span><span class="nx">Server</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">Addr</span><span class="p">:</span><span class="w">              </span><span class="nx">cfg</span><span class="p">.</span><span class="nx">ListenAddr</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">Handler</span><span class="p">:</span><span class="w">           </span><span class="nx">wrappedMux</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">ReadTimeout</span><span class="p">:</span><span class="w">       </span><span class="nx">cfg</span><span class="p">.</span><span class="nx">ReadTimeout</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">ReadHeaderTimeout</span><span class="p">:</span><span class="w"> </span><span class="nx">cfg</span><span class="p">.</span><span class="nx">ReadHeaderTimeout</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">IdleTimeout</span><span class="p">:</span><span class="w">       </span><span class="nx">cfg</span><span class="p">.</span><span class="nx">IdleTimeout</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">TLSConfig</span><span class="p">:</span><span class="w">         </span><span class="nx">cfg</span><span class="p">.</span><span class="nx">TLSConfig</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">server</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Again, a handful of lines, some dependency injection, and baby you&rsquo;ve got a <del>stew</del> server going with routing, limits and authentication. This also demonstrates another pattern I enjoy about Go - imperative programming making things easy to test. Oh right, testing! Unlike many other languages, Go comes with a test harness that is great without external libraries. An example from the above repo:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">TestAllowedCommands</span><span class="p">(</span><span class="nx">t</span><span class="w"> </span><span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">testCases</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">expectedCommands</span><span class="w"> </span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">int</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">desc</span><span class="w">             </span><span class="kt">string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">role</span><span class="w">             </span><span class="nx">Role</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">desc</span><span class="p">:</span><span class="w">             </span><span class="s">&#34;rw&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">role</span><span class="p">:</span><span class="w">             </span><span class="s">&#34;rw&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">expectedCommands</span><span class="p">:</span><span class="w"> </span><span class="nx">AllowedRWCommands</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">desc</span><span class="p">:</span><span class="w">             </span><span class="s">&#34;ro&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">role</span><span class="p">:</span><span class="w">             </span><span class="s">&#34;ro&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">expectedCommands</span><span class="p">:</span><span class="w"> </span><span class="nx">AllowedROCommands</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">desc</span><span class="p">:</span><span class="w">             </span><span class="s">&#34;missing_role&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">role</span><span class="p">:</span><span class="w">             </span><span class="s">&#34;gg&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="nx">expectedCommands</span><span class="p">:</span><span class="w"> </span><span class="kc">nil</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">tC</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">testCases</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">tC</span><span class="p">.</span><span class="nx">desc</span><span class="p">,</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">t</span><span class="w"> </span><span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">if</span><span class="w"> </span><span class="p">!</span><span class="nx">reflect</span><span class="p">.</span><span class="nf">DeepEqual</span><span class="p">(</span><span class="nx">tC</span><span class="p">.</span><span class="nx">expectedCommands</span><span class="p">,</span><span class="w"> </span><span class="nx">tC</span><span class="p">.</span><span class="nx">role</span><span class="p">.</span><span class="nf">AllowedCommands</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">				</span><span class="nx">t</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;expected: %+v, got: %+v&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">tC</span><span class="p">.</span><span class="nx">expectedCommands</span><span class="p">,</span><span class="w"> </span><span class="nx">tC</span><span class="p">.</span><span class="nx">role</span><span class="p">.</span><span class="nf">AllowedCommands</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="p">})</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>This is a <em>table-driven test</em> - a pattern that I don&rsquo;t see used enough, but extremely common in Go codebases.</p>
<p>Go (hah!) spend a week implementing a small proxy, or similar networked service, or work through the fantastic <a href="https://quii.gitbook.io/learn-go-with-tests">Learn Go With Tests</a>.</p>
<h2 id="4-gleam">4. Gleam</h2>
<p>I wanted to include something in the spirit of Erlang on this list, but wanted something a bit more &ldquo;modern&rdquo;. <a href="https://elixir-lang.org/">Elixir</a> would have been the clear choice, but I decided on <a href="https://gleam.run/">Gleam</a>. Gleam is a relative newcomer, but there are some interesting ideas here - and interoperability - that make it worth exploring.</p>
<p>To quote the Gleam page:</p>
<blockquote>
<p>The power of a type system, the expressiveness of functional programming, and the reliability of the highly concurrent, fault tolerant Erlang runtime, with a familiar and modern syntax.</p></blockquote>
<p>Gleam applies many of the lessons learned in programming language design over the years, such as avoiding <a href="https://en.wikipedia.org/wiki/Null_pointer">the billion dollar mistake</a> and including a strong type system (notice a theme?)</p>
<p>Gleam can additionally compile to JavaScript, which is a particularly neat trick.</p>
<p>Here&rsquo;s a small Gleam program that reads some logs in from <code>stdin</code> and outputs them as JSON:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-elixir" data-lang="elixir"><span class="line"><span class="cl"><span class="kn">import</span> <span class="n">gleam</span><span class="o">/</span><span class="n">io</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="n">gleam</span><span class="o">/</span><span class="n">iterator</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="n">gleam</span><span class="o">/</span><span class="n">json</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="n">gleam</span><span class="o">/</span><span class="n">string</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="n">stdin</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">type</span> <span class="nc">LogEntry</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nc">Entry</span><span class="p">(</span><span class="ss">level</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="ss">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">fn</span> <span class="n">entry_to_json</span><span class="p">(</span><span class="ss">entry</span><span class="p">:</span> <span class="nc">LogEntry</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nc">String</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">json</span><span class="o">.</span><span class="n">object</span><span class="p">([</span>
</span></span><span class="line"><span class="cl">    <span class="c1">#(&#34;level&#34;, json.string(entry.level)),</span>
</span></span><span class="line"><span class="cl">    <span class="c1">#(&#34;message&#34;, json.string(entry.message)),</span>
</span></span><span class="line"><span class="cl">  <span class="p">])</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">json</span><span class="o">.</span><span class="n">to_string</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">fn</span> <span class="n">parse_to_entry</span><span class="p">(</span><span class="ss">input</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nc">LogEntry</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="n">string</span><span class="o">.</span><span class="n">split_once</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="ss">on</span><span class="p">:</span> <span class="s2">&#34;:&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nc">Ok</span><span class="p">(</span><span class="n">parts</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nc">Entry</span><span class="p">(</span><span class="ss">level</span><span class="p">:</span> <span class="n">parts</span><span class="o">.</span><span class="mi">0</span><span class="p">,</span> <span class="ss">message</span><span class="p">:</span> <span class="n">string</span><span class="o">.</span><span class="n">trim</span><span class="p">(</span><span class="n">parts</span><span class="o">.</span><span class="mi">1</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nc">Error</span><span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nc">Entry</span><span class="p">(</span><span class="ss">level</span><span class="p">:</span> <span class="s2">&#34;UNKNOWN&#34;</span><span class="p">,</span> <span class="ss">message</span><span class="p">:</span> <span class="n">input</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">//</span> <span class="n">read</span> <span class="n">logs</span> <span class="n">from</span> <span class="n">stdin</span><span class="p">,</span> <span class="n">output</span> <span class="n">as</span> <span class="n">json</span>
</span></span><span class="line"><span class="cl"><span class="o">//</span> <span class="n">input</span> <span class="n">is</span> <span class="n">something</span> <span class="n">like</span> <span class="s2">&#34;ERROR: something went wrong&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">//</span> <span class="n">output</span> <span class="n">is</span> <span class="n">something</span> <span class="n">like</span> <span class="p">{</span><span class="s2">&#34;level&#34;</span><span class="p">:</span> <span class="s2">&#34;ERROR&#34;</span><span class="p">,</span> <span class="s2">&#34;message&#34;</span><span class="p">:</span> <span class="s2">&#34;something went wrong&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">pub</span> <span class="k">fn</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">stdin</span><span class="o">.</span><span class="n">stdin</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">iterator</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">parse_to_entry</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">iterator</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">entry_to_json</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">iterator</span><span class="o">.</span><span class="n">each</span><span class="p">(</span><span class="n">io</span><span class="o">.</span><span class="n">print</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><a href="https://tour.gleam.run/everything/#functions-pipelines">Pipelines</a> are something <em>every</em> functional language should have - so much nicer than nested parens.</p>
<p>We can then export this program as an <code>erlang-shipment</code>, which compiles all the modules ready to be run by any system with Erlang:</p>
<pre tabindex="0"><code>$ gleam export erlang-shipment
  Compiling gleam_stdlib
  Compiling gleam_json
  Compiling gleeunit
  [snip]
</code></pre><p>And then execute it:</p>
<pre tabindex="0"><code>$ printf &#34;ERROR: something went wrong\nINFO: no it didnt&#34; | ./build/erlang-shipment/entrypoint.sh run | jq
{
  &#34;level&#34;: &#34;ERROR&#34;,
  &#34;message&#34;: &#34;something went wrong&#34;
}
{
  &#34;level&#34;: &#34;INFO&#34;,
  &#34;message&#34;: &#34;no it didnt&#34;
}
</code></pre><p>Gleam is a young language, being developed by a small community under the careful stewardship of <a href="https://lpil.uk/">Louis Pilfold</a>. Spend a week helping out on some issues, or consider writing some packages to add to the ecosystem (at least, something more meaningful than <code>left-pad</code>, please).</p>
<h2 id="5-zig">5. Zig</h2>
<blockquote>
<p>I wanna really, really, really wanna &ldquo;zig-a-zig&rdquo;, ah<br>
~ <em>Spice Girls</em></p></blockquote>
<p><a href="https://ziglang.org/">Zig</a> is next up. I was sorely tempted to go with a <a href="https://knowyourmeme.com/memes/it-was-me-dio">&ldquo;You were expecting Rust, but it was me, Zig!&rdquo;</a> opening, but we&rsquo;ll have to do with a meta-commentary instead.</p>
<p>To me, Zig is more interesting than Rust for three primary reasons - <code>comptime</code>, passing allocators, and incremental improvement of C/C++ codebases through Zig. If Rust is a &ldquo;better C++&rdquo;, then Zig is a &ldquo;better C&rdquo;, where the lack of macros etc are a feature, rather than an oversight.</p>
<p>Much like my earlier recommendation of Go, the surface area of Zig is very small, with no hidden control flow and a frankly tiny PEG grammar. It is very much worth your time to read the <a href="https://ziglang.org/learn/overview/">features overview</a> - it covers pretty much all the unique aspects of Zig.</p>
<p>My examples here will be with Zig 0.13.0 - Zig is a reasonably unstable language (folks who write nightly Rust will be familiar) and so these examples may not run with newer or older versions.</p>
<p>First up, a fun <code>comptime</code> trick - initialising a set of static data that would be expensive at runtime (from <a href="https://ziglang.org/documentation/master/#comptime">the <code>comptime</code> documentation</a>):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zig" data-lang="zig"><span class="line"><span class="cl"><span class="kr">const</span><span class="w"> </span><span class="n">first_25_primes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">firstNPrimes</span><span class="p">(</span><span class="mi">25</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kr">const</span><span class="w"> </span><span class="n">sum_of_first_25_primes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">sum</span><span class="p">(</span><span class="o">&amp;</span><span class="n">first_25_primes</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">fn</span><span class="w"> </span><span class="nf">firstNPrimes</span><span class="p">(</span><span class="kr">comptime</span><span class="w"> </span><span class="n">n</span><span class="o">:</span><span class="w"> </span><span class="kt">usize</span><span class="p">)</span><span class="w"> </span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="kt">i32</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">var</span><span class="w"> </span><span class="n">prime_list</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="n">n</span><span class="p">]</span><span class="kt">i32</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">undefined</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">var</span><span class="w"> </span><span class="n">next_index</span><span class="o">:</span><span class="w"> </span><span class="kt">usize</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">var</span><span class="w"> </span><span class="n">test_number</span><span class="o">:</span><span class="w"> </span><span class="kt">i32</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="n">next_index</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">prime_list</span><span class="p">.</span><span class="n">len</span><span class="p">)</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="n">test_number</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kr">var</span><span class="w"> </span><span class="n">test_prime_index</span><span class="o">:</span><span class="w"> </span><span class="kt">usize</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kr">var</span><span class="w"> </span><span class="n">is_prime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">while</span><span class="w"> </span><span class="p">(</span><span class="n">test_prime_index</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">next_index</span><span class="p">)</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="n">test_prime_index</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">test_number</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="n">prime_list</span><span class="p">[</span><span class="n">test_prime_index</span><span class="p">]</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">is_prime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="k">break</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">is_prime</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">prime_list</span><span class="p">[</span><span class="n">next_index</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">test_number</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">next_index</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">prime_list</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">fn</span><span class="w"> </span><span class="nf">sum</span><span class="p">(</span><span class="n">numbers</span><span class="o">:</span><span class="w"> </span><span class="p">[]</span><span class="kr">const</span><span class="w"> </span><span class="kt">i32</span><span class="p">)</span><span class="w"> </span><span class="kt">i32</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kr">var</span><span class="w"> </span><span class="n">result</span><span class="o">:</span><span class="w"> </span><span class="kt">i32</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">numbers</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="n">x</span><span class="o">|</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">result</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="n">x</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">result</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">test</span><span class="w"> </span><span class="s">&#34;variable values&#34;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">try</span><span class="w"> </span><span class="nb">@import</span><span class="p">(</span><span class="s">&#34;std&#34;</span><span class="p">).</span><span class="n">testing</span><span class="p">.</span><span class="nf">expect</span><span class="p">(</span><span class="n">sum_of_first_25_primes</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">1060</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>We can run this with <code>zig test --verbose-llvm-ir=llvm.ir primes.zig</code>, and then inspect the LLVM IR for the compiled values:</p>
<pre tabindex="0"><code>$ rg &#39;@primes\.&#39; llvm.ir
255:@primes.first_25_primes = internal unnamed_addr constant [25 x i32] [i32 2, i32 3, i32 5, i32 7, i32 11, i32 13, i32 17, i32 19, i32 23, i32 29, i32 31, i32 37, i32 41, i32 43, i32 47, i32 53, i32 59, i32 61, i32 67, i32 71, i32 73, i32 79, i32 83, i32 89, i32 97], align 4, !dbg !10
256:@primes.sum_of_first_25_primes = internal unnamed_addr constant i32 1060, align 4, !dbg !11
</code></pre><p>This is a useful trick if you have something like precomputed hash tables, where you can amortise the cost over the life of the program by doing this at compile time. Another example is a <a href="https://ziglang.org/learn/samples/#generic-types">generic queue</a>.</p>
<p>Spend a week re-implementing some basic C programs in Zig, and see how far you get. Something like <a href="https://github.com/coreutils/coreutils/blob/master/src/basename.c"><code>coreutils/basename</code></a> is a small enough domain to work through.</p>
<h2 id="6-racket">6. Racket</h2>
<p><a href="https://racket-lang.org/">Racket</a> is next up - you didn&rsquo;t think I&rsquo;d complete this list without <em>a</em> Lisp, right? Racket is interesting in that unlike pretty much all other languages, Racket is <em>language-oriented</em> - its a language for making languages. Additionally, its strengths lie in its ability to convey programming principles, exemplified in the book <a href="https://htdp.org/"><em>How To Design Programs</em></a>.</p>
<p>Given it&rsquo;s a language to build languages, its no surprise that it has some in-built languages to ease development in specific domains. For example, for a web server, you might consider the <code>web-server/insta</code> language:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-racket" data-lang="racket"><span class="line"><span class="cl"><span class="kn">#lang </span><span class="nn">web-server/insta</span>
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="k">define</span> <span class="p">(</span><span class="n">start</span> <span class="n">request</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="n">response/xexpr</span>
</span></span><span class="line"><span class="cl">   <span class="o">&#39;</span><span class="p">(</span><span class="ss">html</span>
</span></span><span class="line"><span class="cl">     <span class="p">(</span><span class="ss">head</span> <span class="p">(</span><span class="ss">title</span> <span class="s2">&#34;My Blog&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">     <span class="p">(</span><span class="ss">body</span> <span class="p">(</span><span class="ss">h1</span> <span class="s2">&#34;Under construction&#34;</span><span class="p">)))))</span><span class="err">
</span></span></span></code></pre></div><p>We can compile and run this (with compilation optional):</p>
<pre tabindex="0"><code>$ raco make web.rkt
$ racket compiled/web_rkt.zo
Your Web application is running at http://localhost:56355/servlets/standalone.rkt.
Stop this program at any time to terminate the Web Server.
</code></pre><p>Or what about making a language for <a href="https://docs.racket-lang.org/pollen/">web publishing</a> or generating <a href="https://docs.racket-lang.org/brag/">a Racket AST</a>? The concept of language-oriented programming is fascinating to me, especially given that so much of business domains are their own language. We see new DSLs pop up all the time, such as <a href="https://github.com/cedar-policy">Cedar</a> to express access control.</p>
<p>Spend a week writing a language in Racket to solve a trivial problem - I like the example of <a href="https://beautifulracket.com/stacker/intro.html"><code>stacker</code></a> for a stack-based calculator. Why not implement one for Reverse Polish Notation?</p>
<h2 id="7-odin">7. Odin</h2>
<p><a href="https://odin-lang.org/">Odin</a> is a fascinating language that has a bunch in common with Zig, but with some very specific applications. As mentioned in the splash page, <a href="https://jangafx.com/">JangaFX</a> are the creators and extensively use Odin in their applications, which are computer graphics related. As such, Odin holds a pretty unique niche in having bindings for literally all the graphics runtimes out there, from <a href="https://pkg.odin-lang.org/vendor/vulkan/">Vulkan</a> to <a href="https://pkg.odin-lang.org/vendor/wgpu/">WebGPU</a>.</p>
<p>Of course, not just games are made with Odin. A great example is the web version of <a href="https://github.com/colrdavidson/spall-web">Spall</a> - a flamegraph profiler that supports their own native format as well as Google&rsquo;s tracing format.</p>
<p>Rather than give you some demo code, I&rsquo;ll instead link to <a href="https://zylinski.se/posts/gamedev-for-beginners-using-odin-and-raylib-1/">Karl Zylinski&rsquo;s wonderful series</a> where they walk through making a game with Odin and <a href="https://www.raylib.com/">raylib</a>. Making small games is a joy. Spend a week working through that.</p>
<p>If you <em>must</em> have some code, here you go:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-odin" data-lang="odin"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="n">game</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="n">rl</span><span class="w"> </span><span class="s">&#34;vendor:raylib&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">main</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="kd">proc</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">rl</span><span class="p">.</span><span class="n">InitWindow</span><span class="p">(</span><span class="nx">1280</span><span class="p">,</span><span class="w"> </span><span class="nx">720</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;A window appears!&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">for</span><span class="w"> </span><span class="err">!</span><span class="n">rl</span><span class="p">.</span><span class="n">WindowShouldClose</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="n">rl</span><span class="p">.</span><span class="n">BeginDrawing</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="n">rl</span><span class="p">.</span><span class="n">DrawText</span><span class="p">(</span><span class="s">&#34;Look ma, no engine!&#34;</span><span class="p">,</span><span class="w"> </span><span class="nx">190</span><span class="p">,</span><span class="w"> </span><span class="nx">200</span><span class="p">,</span><span class="w"> </span><span class="nx">20</span><span class="p">,</span><span class="w"> </span><span class="n">rl</span><span class="p">.</span><span class="n">BLACK</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="n">rl</span><span class="p">.</span><span class="n">ClearBackground</span><span class="p">(</span><span class="n">rl</span><span class="p">.</span><span class="n">RAYWHITE</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="n">rl</span><span class="p">.</span><span class="n">EndDrawing</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">rl</span><span class="p">.</span><span class="n">CloseWindow</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>In this example, we load the bindings to <code>raylib</code> and create a window, draw some text and set the background colour.</p>
<h2 id="wrap-up">Wrap Up</h2>
<p>We&rsquo;ve covered 7 languages - Python, TypeScript, Go, Gleam, Zig, Racket and Odin - as something to explore in 7 weeks. Maybe one of these will become your daily driver, or you&rsquo;ll make significant contributions to the language or community. Maybe you&rsquo;ll hate them and never touch them again. At the very least, I hope you learn something from exploring them.</p>]]></content:encoded></item><item><title>What Is a Senior Engineer, Anyway?</title><link>https://matt.blwt.io/post/what-is-a-senior-engineer-anyway/</link><pubDate>Sun, 17 Nov 2024 17:45:33 +0000</pubDate><guid>https://matt.blwt.io/post/what-is-a-senior-engineer-anyway/</guid><description>&lt;p&gt;I&amp;rsquo;ve been having a bunch of conversations with my team about our career ladder, and what it means to be &amp;ldquo;senior&amp;rdquo; in a software engineering context. It&amp;rsquo;s a little different in every company, but here is my view.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>I&rsquo;ve been having a bunch of conversations with my team about our career ladder, and what it means to be &ldquo;senior&rdquo; in a software engineering context. It&rsquo;s a little different in every company, but here is my view.</p>
<figure><img src="/what-is-a-senior-engineer-anyway/header.webp">
</figure>

<p>For a long time, I felt the gold standard for a career ladder was <a href="https://docs.google.com/document/d/1SxmQBrDZvj16veuc2OVO0wUX7a7vEKPM-57dNLXhuEk/edit?tab=t.0">RentTheRunway&rsquo;s</a>, shared by <a href="https://www.camilletalk.com/">Camille Fournier</a>. Combined with something like <a href="https://www.levels.fyi/">levels.fyi</a>, we can get a scope of what the &ldquo;median average&rdquo; software engineer looks like at each level.</p>
<p>I&rsquo;m going to take a bit of an unconventional path and talk very little about technology. In an <a href="https://peter.bourgon.org/go-for-industrial-programming/">industrial programming</a> environment, we should be hiring folks who can <em>program</em>.</p>
<p>However you check for that in your interview loop is your choice - I&rsquo;m a fan of the work sample test. This, combined with a strong rubric, helps with levelling.</p>
<p>For the purposes of this article, I&rsquo;m going to use Levels.fyi&rsquo;s &ldquo;standard&rdquo; mapping for levelling to provide an easy comparison. I&rsquo;m also going to be primarily talking about <strong>scope</strong> and <strong>impact</strong>.</p>
<p>I&rsquo;ve worked at companies from ~20 employees through to ~80K employees, and at the time of writing I manage a team that covers everything from <code>L1</code> through the top of <code>L4</code>.</p>
<p>We&rsquo;re going to start at <code>L1</code> and work our way up - the context of previous levels helps frame what &ldquo;senior&rdquo; means.</p>
<h2 id="l1---the-early-career-engineer">L1 - The Early Career Engineer</h2>
<h3 id="scope">Scope</h3>
<p>Congratulations on what is likely their first job as a software engineer! Their scope of responsibility is near zero - they are here to <em>learn</em>. That said, this is a real &ldquo;sink or swim&rdquo; test - I generally don&rsquo;t expect folks to stay at <code>L1</code> longer than one performance review.</p>
<p>At this stage, an <code>L1</code> is expected to spend the majority of their time pairing with more senior engineers and making supervised changes to production code. I generally push with a &ldquo;one week to meaningful PR&rdquo; standard, even if &ldquo;meaningful&rdquo; here is a small bug fix or feature improvement.</p>
<p>The bulk of the time should be learning how software engineering works beyond the classroom and being a sponge. This is a great time to take advantage of any education bursaries / cert funding that the company may offer.</p>
<h3 id="impact">Impact</h3>
<p>Minimal impact expected - the goal is going from being a net-negative addition to the team to break-even as fast as possible.</p>
<h2 id="l2---the-core">L2 - The Core</h2>
<h3 id="scope-1">Scope</h3>
<p><code>L2</code>s are the core of any software engineering team. As per the &ldquo;pyramid distribution&rdquo;, this should be your widest step - <code>L1</code>s don&rsquo;t really count as they should quickly graduate into becoming an <code>L2</code>.</p>
<p>An <code>L2</code> should be considered a wholly independent engineer, able to take well-scoped, small items off the kanban/sprint backlog and deliver to the definition of done without much more supervision than code review. They should be encouraged to share responsibility of delivery of larger, more complex items with another <code>L2</code> or a more senior engineer. They are <em>generally</em> not held accountable of delivery of larger <a href="https://en.wikipedia.org/wiki/Objectives_and_key_results">OKR</a> items, but may be held accountable of delivery of parts of those items.</p>
<p>Learning continues here, but primarily self-driven or through pairing with more senior engineers are on more complex problems. I expect <code>L2</code>s to start engaging in writing culture, and should generally be reading <em>a lot</em> of code and asking questions about <em>why</em> things are done a particular way.</p>
<h3 id="impact-1">Impact</h3>
<p>As mentioned earlier, an <code>L2</code> is generally about delivering small, well-scoped chunks of larger projects delivered by the immediate team. At this stage, I would expect them to start participating within a team&rsquo;s/org&rsquo;s <a href="/post/tools-for-a-culture-of-writing/">culture of writing</a>, if even only as a reviewer rather than instigator.</p>
<h2 id="l3---the-senior-engineer">L3 - The Senior Engineer</h2>
<h3 id="scope-2">Scope</h3>
<p><code>L3</code>s are your go-to executors, leading small groups of <code>L2</code>s to tackle larger, more complex projects - the ones that typically appear as key results in <em>your</em> OKRs (assuming you are a line manager/tech lead).</p>
<p>On top of the skills of <code>L2</code>, this is where I&rsquo;d expect to start to see some level of specialisation - deep knowledge of a specific domain, and understanding how to apply that knowledge for the greatest effect. I also expect <code>L3</code>s to evolve beyond being an order-taker and start to generate work for themselves - improving the codebase, practices and so on. Similarly, I expect them to engage in writing culture, gathering consensus where decisions need to be made that impact the immediate team.</p>
<p>On top of that, I expect <code>L3</code>s to own delivery of small-medium projects from design through to delivery, with help from <code>L4</code>s and up. They should also ensure they are assisting in the growth of <code>L2</code>s into more <code>L3</code>s.</p>
<h3 id="impact-2">Impact</h3>
<p><code>L3</code>s are &ldquo;get-it-done&rdquo; folks - they are given a task and drive it to completion in a high quality way. Their contributions should always be beyond the immediate scope of the task they took on - becoming somewhat of a &ldquo;1.1x&rdquo; engineer.</p>
<p>Beyond that, this is where working <em>only</em> within the scope of the immediate team becomes a career limiting factor. To progress beyond that, scope and impact needs to exceed that of the immediate team boundaries.</p>
<p>Generally, I would expect an <code>L3</code> and above to &ldquo;go a little further&rdquo; on everything - kind of like practising &ldquo;5 Why&rsquo;s&rdquo; for each thing. This is especially true for things like requirements.</p>
<p>I would consider this a terminal level for some engineers.</p>
<h2 id="l4---the-staff-engineer">L4 - The Staff Engineer</h2>
<h3 id="scope-3">Scope</h3>
<p><code>L4</code> gets in the murky world of &ldquo;staff&rdquo; engineering. At this level, scope expands beyond the immediate team and there is an expectation to own delivery of larger, complex projects that cut across many teams within a business function. <code>L4</code>s are likely wearing different hats different days of the week, and should generally be part of planning and longer term initiatives. &ldquo;Planting trees they will not sit in&rdquo; is a good mantra for the scope of work an L4 will be doing.</p>
<p>This is where it gets much harder to define what the appropriate scope is. In my experience, an <code>L4</code> is generally owning design-through-delivery of projects that appear at the Senior Director/Vice President level. They are also where the buck typically stops for technical decision making within their team. For many, the scope is self-defining.</p>
<h3 id="impact-3">Impact</h3>
<p><code>L4</code>s should have outsized impact with most things they do - their job is making the whole team more effective and are essentially a right hand to the leaders of the team, if not a business unit leader. I would expect frequent contributions to writing culture, and ensuring that there is a focus on outcomes rather than output.</p>
<p>This is a terminal level for the majority of engineers.</p>
<h2 id="l5---architects-principals-etc">L5+ - Architects, Principals, Etc</h2>
<p>Beyond the <code>L4</code>, it gets harder to define the role according to of scope and impact so narrowly. Generally speaking, an <code>L5</code> owns technical direction for a whole business unit or many business units, and are typically technical &ldquo;faces&rdquo; of an organisation at conferences and other industry events.</p>
<p>How an org defines this level is very org dependent, and so becomes hard to generalise. Ask your friendly neighbourhood architect what <em>they</em> see the role as - you&rsquo;ll probably get different answers from different architects, even under the same org.</p>
<p>As this level rarely leaves time for day-to-day coding, many <code>L4</code>s are happy not stepping into it. There is also a level of politics at play at this level that is normally only exposed to the management track, which some engineers might also find unpalatable.</p>
<h2 id="wrap-up">Wrap Up</h2>
<p>So what is a senior engineer, anyway? It&rsquo;s about scope and impact. Once an engineer takes on a wider scope of responsibility and is driving outcomes that are larger than they are, its safe to call them senior.</p>
<p>And how should we manage them? Generally by getting out of their way, by laying down track for them to follow to the next level, providing opportunities to own bigger things, and the support necessary to make them successful.</p>]]></content:encoded></item><item><title>Regular Restarts Are Good, Actually</title><link>https://matt.blwt.io/post/regular-restarts-are-good-actually/</link><pubDate>Fri, 08 Nov 2024 12:51:16 +0000</pubDate><guid>https://matt.blwt.io/post/regular-restarts-are-good-actually/</guid><description>&lt;p&gt;Anecdotally, one of the more maligned features of the Heroku platform are the &lt;a href="https://devcenter.heroku.com/articles/dyno-restarts#automatic-restarts"&gt;24-hour limits on compute units, known as &amp;ldquo;dynos&amp;rdquo;&lt;/a&gt;. This is actually a good thing, but very misunderstood.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>Anecdotally, one of the more maligned features of the Heroku platform are the <a href="https://devcenter.heroku.com/articles/dyno-restarts#automatic-restarts">24-hour limits on compute units, known as &ldquo;dynos&rdquo;</a>. This is actually a good thing, but very misunderstood.</p>
<figure><img src="/regular-restarts-are-good-actually/header.webp">
</figure>

<p>Regular restarts of compute units bring one obvious benefit - resolving memory leaks. Broadly, this is what the documentation means by &ldquo;maintain the health of applications&rdquo; running on the platform. Slow memory leaks are insidious, only appearing when taking a sufficiently zoomed out view of memory utilisation by the application, and a regular restart is one way of papering over that behaviour.</p>
<p>Historically, <a href="https://rubyonrails.org/">Rails</a> applications used to be particularly hit by this problem, mostly due to how easy it is to accumulate objects. Real ones will remember the introduction of <code>frozen_string_literal: true</code> magic comment. Given Heroku&rsquo;s <a href="https://blog.heroku.com/whats_up_at_heroku">history as a home for Rails apps</a>, there should be no surprise as to why this regular cycling exists.</p>
<p>But there are other benefits, and for that, we need to re-visit <a href="https://12factor.net/">The Twelve-Factor App</a>.</p>
<p>The Twelve-Factor App is a little long in the tooth - Heroku CTO Gail Frederick is speaking about what a <a href="https://sched.co/1ndb8">re-imagined set would look like for today at KubeCon Americas 2024</a> - but there are two factors that remain as powerful today as they did 13 years ago: <a href="https://12factor.net/processes"><strong>Processes</strong></a> and <a href="https://12factor.net/disposability"><strong>Disposability</strong></a>.</p>
<h2 id="the-art-of-throwing-things-away">The Art of Throwing Things Away</h2>
<p>The combination of an ephemeral local filesystem and regular restarts means that you will not have local state and you will like it, dammit. And honestly, no local state is the best way to be - don&rsquo;t accumulate cruft.</p>
<p>By enforcing the lack of local state, we can ensure that state lives somewhere more appropriate - a queue, a database, an object store and so on. This makes coordination significantly easier - shared-nothing services can be thrown away, scaled up and down without having to worry about state.</p>
<p>After all - <a href="https://x.com/rakyll/status/1291793630466695168">always make state and coordination someone else&rsquo;s problem</a>.</p>
<p>We can take this concept further and architect our processes under the assumption that they can, and will, be interrupted <em>at any time</em>. This is why this &ldquo;feature&rdquo; gets a lot of flack - what to do about longer-running tasks?</p>
<p>If we&rsquo;re fully embracing this model, we&rsquo;re putting state elsewhere, like a queue. It&rsquo;s entirely possible you will be mid-way through running a job and have your compute unit terminated. This introduces us to the architectural properties we need - <strong>idempotency</strong>, <strong>atomicity</strong> and <strong>reentrancy</strong>.</p>
<p>I&rsquo;ve <a href="/post/unreasonably-effective-patterns">written about this elsewhere</a>. By ensuring a given operation is idempotent and atomic, and the collection of operations are reentrant, we don&rsquo;t need to concern ourselves mid-task interruptions - &ldquo;just&rdquo; retry. Embrace &ldquo;at least once&rdquo; processing. Kill your darlings. Let the cruft be swept away by a pod reaper or the dyno scheduler. By ensuring repeatable, predictable side effects and the ability to safely resume processing from a given state without leaving half-baked results lying around, we reduce our headaches considerably.</p>
<h2 id="time-keeps-on-slipping">Time Keeps On Slipping</h2>
<p>But what about tasks that are going to take a long amount of time anyway? The classic example is the &ldquo;billing run&rdquo; or similar end-of-month batch processes that require loading larger volumes of data into memory, doing some number crunching, then spitting out a result.</p>
<p>For these, we must instead decompose into smaller tasks. Providing an <em>intermediate representation</em> that can be stored (again, not locally!) somewhere and loaded, while keeping track of where in the sequence we are to resume processing after cancellation. If you are thinking &ldquo;this sounds a lot like the MapReduce pattern&rdquo; - you are right! MapReduce isn&rsquo;t just for &ldquo;big data&rdquo;.</p>
<p>Then we run into the hard problem - long tasks that cannot be broken up into small, atomic tasks. In my line of work, the most familiar is the <em>database dump</em>.</p>
<p>Although Postgres natively supports backing up through the type of process outlined above (through continuous WAL archival, enabling &ldquo;hot&rdquo; backups), this isn&rsquo;t very useful if you just want the <em>data</em>. Outside of logical replication, typically the only alternative is to use something like <code>pg_dump</code>.</p>
<p>Unfortunately, dumping a database, or even parts of a database, using <code>pg_dump</code> is a monolithic task - there are no valid &ldquo;partial&rdquo; dumps due to needing access to the transaction snapshot, which if <code>pg_dump</code> is interrupted, no longer exists. For very large datasets, it is entirely possible that even on adequate hardware and running in parallel mode <code>pg_dump</code>, the odds of the process being interrupted are very high.</p>
<p>So what to do about these monolithic tasks? &ldquo;Just don&rsquo;t restart&rdquo; I hear you cry. And for these very particular types of work, you are correct. However, such behaviour should be <em>opt-in</em> instead of <em>opt-out</em> - discourage this kind of task in favour of composable sequences of tasks.</p>
<p><em>Update 2024-11-09: An earlier version of this post referred to Gail Frederick as Heroku CEO instead of Heroku CTO. This has been corrected.</em></p>]]></content:encoded></item><item><title>On That Okta LDAP Bug</title><link>https://matt.blwt.io/post/on-that-okta-ldap-bug/</link><pubDate>Sun, 03 Nov 2024 12:51:00 +0000</pubDate><guid>https://matt.blwt.io/post/on-that-okta-ldap-bug/</guid><description>&lt;p&gt;A quick explanation of the Okta AD/LDAP DelAuth bug that was being shared around, and the importance of sensible defaults.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>A quick explanation of the Okta AD/LDAP DelAuth bug that was being shared around, and the importance of sensible defaults.</p>
<figure><img src="/on-that-okta-ldap-bug/header.webp">
</figure>

<p>So <a href="https://trust.okta.com/security-advisories/okta-ad-ldap-delegated-authentication-username/">this Okta security advisory</a> was doing the rounds, and was pretty transparent, all things considered:</p>
<blockquote>
<p>On October 30, 2024, a vulnerability was internally identified in generating the cache key for AD/LDAP DelAuth. The Bcrypt algorithm was used to generate the cache key where we hash a combined string of userId + username + password. During specific conditions, this could allow users to authenticate by only providing the username with the stored cache key of a previous successful authentication.</p></blockquote>
<p><code>bcrypt</code> has a <a href="https://en.wikipedia.org/wiki/Bcrypt#Maximum_password_length">well-known limitation</a> that it has a maximum password length of 72 bytes (or 72 ASCII characters). By using <code>bcrypt</code> as a hash function for a cache key that is composed by concatenation, we can infer that <code>clientId</code> is 20 bytes, and therefore a username of 52 or more bytes results in the password becoming irrelevant.</p>
<p>We can easily demonstrate this in code:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">bcrypt</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">secrets</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client_id</span> <span class="o">=</span> <span class="n">secrets</span><span class="o">.</span><span class="n">token_hex</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">client_username</span> <span class="o">=</span> <span class="n">secrets</span><span class="o">.</span><span class="n">token_hex</span><span class="p">(</span><span class="mi">26</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">client_password</span> <span class="o">=</span> <span class="n">secrets</span><span class="o">.</span><span class="n">token_hex</span><span class="p">(</span><span class="mi">32</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">cache_key</span> <span class="o">=</span> <span class="n">client_id</span> <span class="o">+</span> <span class="n">client_username</span> <span class="o">+</span> <span class="n">client_password</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">hashed_key</span> <span class="o">=</span> <span class="n">bcrypt</span><span class="o">.</span><span class="n">hashpw</span><span class="p">(</span><span class="n">cache_key</span><span class="o">.</span><span class="n">encode</span><span class="p">(),</span> <span class="n">bcrypt</span><span class="o">.</span><span class="n">gensalt</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">check_password</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">check_key</span> <span class="o">=</span> <span class="n">client_id</span> <span class="o">+</span> <span class="n">client_username</span> <span class="o">+</span> <span class="n">check_password</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Correct Password: </span><span class="si">{}</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">client_password</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Entered Password: </span><span class="si">{}</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">check_password</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">bcrypt</span><span class="o">.</span><span class="n">checkpw</span><span class="p">(</span><span class="n">check_key</span><span class="o">.</span><span class="n">encode</span><span class="p">(),</span> <span class="n">hashed_key</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Password is correct.&#34;</span><span class="p">)</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ python main.py foobar
</span></span><span class="line"><span class="cl">Correct Password: 3b34021604ace062aa21a77f3461dc0289191988c248467f2401c9540939ec5c
</span></span><span class="line"><span class="cl">Entered Password: foobar
</span></span><span class="line"><span class="cl">Password is correct.
</span></span></code></pre></div><p>Alternatively, you can hash the input with a constant-length hash function, like <a href="https://en.wikipedia.org/wiki/SHA-2">SHA-256</a>, producing a 64 character hex digest:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gu">@@ -1,4 +1,5 @@
</span></span></span><span class="line"><span class="cl"><span class="gu"></span> import bcrypt
</span></span><span class="line"><span class="cl"><span class="gi">+import hashlib
</span></span></span><span class="line"><span class="cl"><span class="gi"></span> import secrets
</span></span><span class="line"><span class="cl"> import sys
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">@@ -7,14 +8,18 @@ client_username = secrets.token_hex(26)
</span></span></span><span class="line"><span class="cl"><span class="gu"></span> client_password = secrets.token_hex(32)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> cache_key = client_id + client_username + client_password
</span></span><span class="line"><span class="cl"><span class="gi">+hashed_input = hashlib.sha256(cache_key.encode()).hexdigest()
</span></span></span><span class="line"><span class="cl"><span class="gi"></span>
</span></span><span class="line"><span class="cl"><span class="gd">-hashed_key = bcrypt.hashpw(cache_key.encode(), bcrypt.gensalt())
</span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+hashed_key = bcrypt.hashpw(hashed_input.encode(), bcrypt.gensalt())
</span></span></span><span class="line"><span class="cl"><span class="gi"></span>
</span></span><span class="line"><span class="cl"> check_password = sys.argv[1]
</span></span><span class="line"><span class="cl"> check_key = client_id + client_username + check_password
</span></span><span class="line"><span class="cl"><span class="gi">+hashed_check_input = hashlib.sha256(check_key.encode()).hexdigest()
</span></span></span><span class="line"><span class="cl"><span class="gi"></span>
</span></span><span class="line"><span class="cl"> print(&#34;Correct Password: {}&#34;.format(client_password))
</span></span><span class="line"><span class="cl"> print(&#34;Entered Password: {}&#34;.format(check_password))
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gd">-if bcrypt.checkpw(check_key.encode(), hashed_key):
</span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+if bcrypt.checkpw(hashed_check_input.encode(), hashed_key):
</span></span></span><span class="line"><span class="cl"><span class="gi"></span>     print(&#34;Password is correct.&#34;)
</span></span></code></pre></div><p>Of course, this introduces a possibility of a collision attack, though at the time of writing not feasible. Another problem with this approach is <a href="https://www.youtube.com/watch?v=OQD3qDYMyYQ">password shucking</a>, as well as a <a href="https://blog.ircmaxell.com/2015/03/security-issue-combining-bcrypt-with.html">litany of other issues of combining <code>bcrypt</code> with other hash functions</a>. Just use a better hash algorithm designed for password storage.</p>
<p>Interestingly, this was &ldquo;fixed&rdquo; by changing the hash function to use <a href="https://en.wikipedia.org/wiki/PBKDF2">PBKDF2</a> - this is materially worse than <code>bcrypt</code>, but it <em>is</em> FIPS compliant. I can only hope this was implemented with a minimum work factor of 600K and using HMAC-SHA-256 as the internal hash function. My preference would be to use <code>argon2id</code> or <code>scrypt</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">argon2</span> <span class="kn">import</span> <span class="n">PasswordHasher</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">argon2.low_level</span> <span class="kn">import</span> <span class="n">Type</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">argon2.exceptions</span> <span class="kn">import</span> <span class="n">VerifyMismatchError</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">secrets</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client_id</span> <span class="o">=</span> <span class="n">secrets</span><span class="o">.</span><span class="n">token_hex</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">client_username</span> <span class="o">=</span> <span class="n">secrets</span><span class="o">.</span><span class="n">token_hex</span><span class="p">(</span><span class="mi">26</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">client_password</span> <span class="o">=</span> <span class="n">secrets</span><span class="o">.</span><span class="n">token_hex</span><span class="p">(</span><span class="mi">32</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">cache_key</span> <span class="o">=</span> <span class="n">client_id</span> <span class="o">+</span> <span class="n">client_username</span> <span class="o">+</span> <span class="n">client_password</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ph</span> <span class="o">=</span> <span class="n">PasswordHasher</span><span class="p">(</span><span class="nb">type</span><span class="o">=</span><span class="n">Type</span><span class="o">.</span><span class="n">ID</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">hashed_key</span> <span class="o">=</span> <span class="n">ph</span><span class="o">.</span><span class="n">hash</span><span class="p">(</span><span class="n">cache_key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">check_password</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">check_key</span> <span class="o">=</span> <span class="n">client_id</span> <span class="o">+</span> <span class="n">client_username</span> <span class="o">+</span> <span class="n">check_password</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Correct Password: </span><span class="si">{}</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">client_password</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Entered Password: </span><span class="si">{}</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">check_password</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">ph</span><span class="o">.</span><span class="n">verify</span><span class="p">(</span><span class="n">hashed_key</span><span class="p">,</span> <span class="n">check_key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Password is correct.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">except</span> <span class="n">VerifyMismatchError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Password is incorrect.&#34;</span><span class="p">)</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ python argon2id.py foobar
</span></span><span class="line"><span class="cl">Correct Password: 720d5d3959f20cbee3f05efc7289f4c307ae7add38c821cfa5708631f4ee77c6
</span></span><span class="line"><span class="cl">Entered Password: foobar
</span></span><span class="line"><span class="cl">Password is incorrect
</span></span></code></pre></div><p>Read <a href="https://www.latacora.com/blog/2024/07/29/crypto-right-answers-pq/">this post from Latacora</a> for more &ldquo;cryptographic right answers&rdquo;.</p>
<p>The use of <code>bcrypt</code> here with an input that is not well restricted and can easily blow through the known 72-byte limitation is a flaw that <em>should</em> have been caught through design review, but I can understand why - if you don&rsquo;t have the cryptographic expertise, this is easily missed.</p>
<p>That said, some <code>bcrypt</code> libraries refuse to accept inputs longer than 72 bytes, such as <a href="https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword">the Go implementation</a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="s">&#34;fmt&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">bcrypt</span><span class="w"> </span><span class="s">&#34;golang.org/x/crypto/bcrypt&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">password</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;894d8772dcff261840ba07e6ceb271a3803483a1900c2719c76561de735b0dab5318c6895085675d5ee315cf10c0a1a6df4e3bbc7681d2ea502ca4de899b6195de410b20&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;password_length: &#34;</span><span class="p">,</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">password</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">bcrypt</span><span class="p">.</span><span class="nf">GenerateFromPassword</span><span class="p">(</span><span class="nx">password</span><span class="p">,</span><span class="w"> </span><span class="nx">bcrypt</span><span class="p">.</span><span class="nx">DefaultCost</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ go run main.go
</span></span><span class="line"><span class="cl">password_length:  <span class="m">136</span>
</span></span><span class="line"><span class="cl">panic: bcrypt: password length exceeds <span class="m">72</span> bytes
</span></span></code></pre></div><p>This reinforces the need to have <a href="/post/sensibly-default">sensible defaults</a>.</p>
<p><em>Update 2024-11-03: include SHA-2 hashing as alternative mitigation.</em></p>]]></content:encoded></item><item><title>Sensibly Default</title><link>https://matt.blwt.io/post/sensibly-default/</link><pubDate>Sat, 19 Oct 2024 14:50:26 +0100</pubDate><guid>https://matt.blwt.io/post/sensibly-default/</guid><description>&lt;p&gt;There are two programming principles that I hold dear to my heart: the &lt;em&gt;principle of least surprise&lt;/em&gt; and &lt;em&gt;provide sensible defaults&lt;/em&gt;. I&amp;rsquo;ve recently been working within the GraphQL ecosystem, and the number of violations of both here has frustrated me. This will be a little bit ranty.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>There are two programming principles that I hold dear to my heart: the <em>principle of least surprise</em> and <em>provide sensible defaults</em>. I&rsquo;ve recently been working within the GraphQL ecosystem, and the number of violations of both here has frustrated me. This will be a little bit ranty.</p>
<figure><img src="/sensibly-default/header.webp">
</figure>

<h2 id="graphql-is-insecure-by-default">GraphQL Is Insecure By Default</h2>
<p>Well, at least in the <a href="https://github.com/graphql/graphql-js/">JavaScript implementation</a>. Cue the bug bounties:</p>
<ul>
<li><a href="https://hackerone.com/reports/1132803">GraphQL Information Disclosure</a></li>
<li><a href="https://hackerone.com/reports/489146">GraphQL Information Disclosure</a></li>
<li><a href="https://hackerone.com/reports/291531">GraphQL Information Disclosure</a></li>
<li><a href="https://hackerone.com/reports/481518">GraphQL Incorrect Cost Handling</a></li>
<li><a href="https://hackerone.com/reports/2048725">GraphQL Denial of Service</a></li>
</ul>
<p>It is <a href="https://blog.aquilosec.com/posts/pentesters-guide-to-graphql">well known</a> that there are several easily accessible Denial of Service vectors on GraphQL endpoints.</p>
<h3 id="unlimited-power-tokens">Unlimited <del>Power</del> Tokens</h3>
<p>One of the most egregious was <a href="https://github.com/graphql-java/graphql-java/issues/2888">CVE-2022-37734</a>, where it is possible to construct a query with a very large number of tokens to put pressure on the lexer - a variant of the <a href="https://en.wikipedia.org/wiki/Billion_laughs_attack">Billion Laughs attack</a>. This <a href="https://github.com/graphql-java/graphql-java/pull/2892">was fixed</a> in <code>graphql-java</code>, by stopping parsing after the configured maximum number of tokens (<em>including whitespace</em>) is reached, extending the original fix (<a href="https://github.com/graphql-java/graphql-java/pull/2549">https://github.com/graphql-java/graphql-java/pull/2549</a>) that adds a default limit of 15,000 tokens.</p>
<p>However, in the reference implementation <a href="https://github.com/graphql/graphql-js/"><code>graphql/graphql-js</code></a>, a <a href="https://github.com/graphql/graphql-js/pull/3702">similar fix was added</a> that did <em>not</em> include a default limit - by default it is left undefined, and so <strong>the DoS mitigation is not active by default</strong>.</p>
<p>Some other implementations also lack this:</p>
<ul>
<li><a href="https://github.com/graphql-python/graphql-core/blob/4e83d4201a2be88832f24c5733c0a1b8568c3708/src/graphql/language/parser.py#L243">graphql-python/graphql-core</a></li>
<li><a href="https://github.com/absinthe-graphql/absinthe/blob/e37e2858c330bda08cbdb1d62f7c6fa7b3a1ca32/lib/absinthe/lexer.ex#L239">absinthe-graphql/absinthe</a></li>
</ul>
<p>There are no doubt other implementations of the spec that don&rsquo;t set this by default.</p>
<p>In many cases this can be mitigated by setting a limit on the size of the request body, but having it configured and built into the lexer is vital.</p>
<h3 id="aliases-and-directives">Aliases and Directives</h3>
<p>As mentioned in the blog post earlier, a less easily solved problem is that of <a href="https://checkmarx.com/blog/alias-and-directive-overloading-in-graphql/">alias overloading and directive overloading</a>.</p>
<p>In short, <em>alias overloading</em> allows performing the same operation multiple times within the same request - <a href="https://lab.wallarm.com/graphql-batching-attack/">batching</a>. <em>Directive overloading</em> &ldquo;spams&rdquo; the same directive multiple times in the same operation.</p>
<p>In order to mitigate against these, we need to traverse the parsed query AST and count against them. Again, this is something that most implementations do not have guards for by default, often falling back to <a href="https://github.com/slicknode/graphql-query-complexity/">query complexity analysis</a>, as well as complex rate limiting implementations, <a href="https://docs.github.com/en/graphql/overview/resource-limitations/">such as GitHub&rsquo;s</a>. I&rsquo;ll wait while you digest <em>that</em> particular document. However, there be dragons: <a href="https://hackerone.com/reports/481518">GraphQL Incorrect Cost Handling</a> disclosed to Shopify indicates that this is a hard thing to get right.</p>
<p>Some popular GraphQL frameworks such as <a href="https://docs.redwoodjs.com/docs/graphql/">RedwoodJS</a> and <a href="https://www.apollographql.com/">Apollo</a> provide some defaults, but in the case of the latter, this is an <strong>Enterprise only feature</strong>.</p>
<p>Again, this kind of thing is surprising.</p>
<h2 id="information-disclosure-by-default">Information Disclosure By Default</h2>
<p>Many GraphQL implementations enable <a href="https://graphql.org/learn/introspection/">schema introspection</a> by default, which allows attackers to perform easy reconnaissance.</p>
<p>It is considered good practice to <a href="https://www.apollographql.com/blog/why-you-should-disable-graphql-introspection-in-production">disable introspection in production</a>, and requires an explicit opt in. The linked docs from Apollo show how for their framework, and in vanilla <code>graphql/graphql-js</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">createHandler</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;graphql-http/lib/use/express&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">NoSchemaIntrospectionCustomRule</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;graphql&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">graphQLHandler</span> <span class="o">=</span> <span class="nx">createHandler</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">validationRules</span><span class="o">:</span> <span class="p">[</span><span class="nx">NoSchemaIntrospectionCustomRule</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p>I <em>kind of</em> understand why this is enabled by default - it <em>is</em> helpful in development - but like a lot of things GraphQL, feels like a loaded footgun.</p>
<h3 id="introspection-abuse">Introspection Abuse</h3>
<p>Besides introspection leaking data, it can also be used in attacks that result in <a href="https://blog.aquilosec.com/posts/pentesters-guide-to-graphql/#deep-recursion--circular-query-via-introspection">deeply recursive queries</a>. As a result, controlling query depth is also required. Reviewing <a href="https://github.com/graphql-python/graphene/blob/dca31dc61d5262444293cd7f59222341b6060b6e/docs/execution/queryvalidation.rst#L15"><code>graphql-python/graphene</code></a>, this is something that needs to be enabled explicitly through an additional validator.</p>
<h2 id="what-does-a-secure-by-default-endpoint-looking-like-anyway">What Does A &ldquo;Secure By Default&rdquo; Endpoint Looking Like Anyway?</h2>
<p>Taking <code>graphql/graphql-js</code> as the reference implementation, lets take the &ldquo;getting started&rdquo; example and work on making it secure.</p>
<p>Here is the starting example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">express</span> <span class="kr">from</span> <span class="s2">&#34;express&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">morgan</span> <span class="kr">from</span> <span class="s2">&#34;morgan&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">createHandler</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;graphql-http/lib/use/express&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">schema</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;./schema&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">root</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">hello</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="s2">&#34;Hello, world!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">morgan</span><span class="p">(</span><span class="s2">&#34;common&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="s2">&#34;/graphql&#34;</span><span class="p">,</span> <span class="nx">createHandler</span><span class="p">({</span> <span class="nx">schema</span><span class="p">,</span> <span class="nx">rootValue</span>: <span class="kt">root</span> <span class="p">}));</span>
</span></span><span class="line"><span class="cl"><span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="mi">3000</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Server running on port 3000&#34;</span><span class="p">));</span>
</span></span></code></pre></div><p>First of all, we need to protect ourselves against large request bodies. Straight forward enough with <code>body-parser</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> import morgan from &#34;morgan&#34;
</span></span><span class="line"><span class="cl"> import { createHandler } from &#34;graphql-http/lib/use/express&#34;
</span></span><span class="line"><span class="cl"> import { schema } from &#34;./schema&#34;
</span></span><span class="line"><span class="cl"><span class="gi">+import bodyParser from &#34;body-parser&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi"></span>
</span></span><span class="line"><span class="cl"> const root = {
</span></span><span class="line"><span class="cl">     hello: () =&gt; &#34;Hello, world!&#34;
</span></span><span class="line"><span class="cl"><span class="gu">@@ -10,5 +11,7 @@ const root = {
</span></span></span><span class="line"><span class="cl"><span class="gu"></span> const app = express()
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> app.use(morgan(&#34;common&#34;))
</span></span><span class="line"><span class="cl"><span class="gi">+app.use(bodyParser.json({ limit: &#34;64kb&#34; }))
</span></span></span><span class="line"><span class="cl"><span class="gi">+app.use(bodyParser.urlencoded({ extended: true, limit: &#34;64kb&#34; }))
</span></span></span><span class="line"><span class="cl"><span class="gi"></span> app.use(&#34;/graphql&#34;, createHandler({ schema, rootValue: root }))
</span></span><span class="line"><span class="cl"> app.listen(3000, () =&gt; console.log(&#34;Server running on port 3000&#34;))
</span></span></code></pre></div><p>Next, we need to add some default validation rules and set our <code>maxToken</code> to some sensible value:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">-app.use(morgan(&#34;common&#34;))
</span></span></span><span class="line"><span class="cl"><span class="gd">-app.use(bodyParser.json({ limit: &#34;64kb&#34; }))
</span></span></span><span class="line"><span class="cl"><span class="gd">-app.use(bodyParser.urlencoded({ extended: true, limit: &#34;64kb&#34; }))
</span></span></span><span class="line"><span class="cl"><span class="gd">-app.use(&#34;/graphql&#34;, createHandler({ schema, rootValue: root }))
</span></span></span><span class="line"><span class="cl"><span class="gd">-app.listen(3000, () =&gt; console.log(&#34;Server running on port 3000&#34;))
</span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+const handler = createHandler({
</span></span></span><span class="line"><span class="cl"><span class="gi">+  schema,
</span></span></span><span class="line"><span class="cl"><span class="gi">+  parse: (query) =&gt; parse(query, { maxTokens: 5000 }),
</span></span></span><span class="line"><span class="cl"><span class="gi">+  validationRules: [
</span></span></span><span class="line"><span class="cl"><span class="gi">+    ...specifiedRules,
</span></span></span><span class="line"><span class="cl"><span class="gi">+    NoSchemaIntrospectionCustomRule,
</span></span></span><span class="line"><span class="cl"><span class="gi">+  ],
</span></span></span><span class="line"><span class="cl"><span class="gi">+});
</span></span></span><span class="line"><span class="cl"><span class="gi">+
</span></span></span><span class="line"><span class="cl"><span class="gi">+app.use(morgan(&#34;common&#34;));
</span></span></span><span class="line"><span class="cl"><span class="gi">+app.use(bodyParser.json({ limit: &#34;64kb&#34; }));
</span></span></span><span class="line"><span class="cl"><span class="gi">+app.use(bodyParser.urlencoded({ extended: true, limit: &#34;64kb&#34; }));
</span></span></span><span class="line"><span class="cl"><span class="gi">+app.post(&#34;/graphql&#34;, handler);
</span></span></span></code></pre></div><p>Now, we need to add some specific guards. The nice people over at <a href="https://escape.tech">escape.tech</a> have created a practically mandatory package - <code>graphql-armor</code> - that exposes some helpful rules to mitigate the above:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> validationRules: [
</span></span><span class="line"><span class="cl">   ...specifiedRules,
</span></span><span class="line"><span class="cl">   NoSchemaIntrospectionCustomRule,
</span></span><span class="line"><span class="cl"><span class="gi">+    maxDepthRule({
</span></span></span><span class="line"><span class="cl"><span class="gi">+      exposeLimits: false,
</span></span></span><span class="line"><span class="cl"><span class="gi">+    }),
</span></span></span><span class="line"><span class="cl"><span class="gi">+    costLimitRule({
</span></span></span><span class="line"><span class="cl"><span class="gi">+      exposeLimits: false,
</span></span></span><span class="line"><span class="cl"><span class="gi">+    }),
</span></span></span><span class="line"><span class="cl"><span class="gi">+    maxDirectivesRule({
</span></span></span><span class="line"><span class="cl"><span class="gi">+      n: config.graphqlMaxDirectives,
</span></span></span><span class="line"><span class="cl"><span class="gi">+      exposeLimits: false,
</span></span></span><span class="line"><span class="cl"><span class="gi">+    }),
</span></span></span><span class="line"><span class="cl"><span class="gi">+    maxAliasesRule({
</span></span></span><span class="line"><span class="cl"><span class="gi">+      n: config.graphqlMaxAliases,
</span></span></span><span class="line"><span class="cl"><span class="gi">+      exposeLimits: false,
</span></span></span><span class="line"><span class="cl"><span class="gi">+    }),
</span></span></span><span class="line"><span class="cl"><span class="gi"></span> ],
</span></span></code></pre></div><p>We set <code>exposeLimits</code> to <code>false</code> to prevent leaking information to attackers about what limits are in place.</p>
<p>The final setup is shown in this repo: <a href="https://github.com/mble/graphql-tirefire"><code>mble/graphql-tirefire</code></a>.</p>
<p>This leaves us with something very similar to <a href="https://github.com/redwoodjs/redwood/blob/ba146c7268c2a974eb85026be49a1c5a6fdf3600/packages/graphql-server/src/plugins/useArmor.ts#L26-L51">RedwoodJS&rsquo;s configuration</a>.</p>
<p>In an ideal world, these packages and their default configuration would be baked into the underlying GraphQL implementations themselves, to provide more of a &ldquo;zero config&rdquo; approach.</p>
<p>By providing sensible defaults, we can enable our users to be more successful out of the gate, and keep the web safer by default.</p>]]></content:encoded></item><item><title>Long Distance Relationships</title><link>https://matt.blwt.io/post/long-distance-relationships/</link><pubDate>Sat, 12 Oct 2024 12:46:27 +0100</pubDate><guid>https://matt.blwt.io/post/long-distance-relationships/</guid><description>&lt;p&gt;I&amp;rsquo;ve been managing a fully remote, fully distributed team, covering timezones from UTC-8 through UTC+10, for the last couple of years. Through that time, I&amp;rsquo;ve learned a lot on organising work, interpersonal relationships, and ultimately how to overcome a lack of promximity to my reports.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>I&rsquo;ve been managing a fully remote, fully distributed team, covering timezones from UTC-8 through UTC+10, for the last couple of years. Through that time, I&rsquo;ve learned a lot on organising work, interpersonal relationships, and ultimately how to overcome a lack of promximity to my reports.</p>
<figure><img src="/long-distance-relationships/header.webp">
</figure>

<h2 id="undercommunication-will-destroy-you">Undercommunication Will Destroy You</h2>
<p>Shortly after I met my partner, circumstances required us to maintain a long distance relationship between London, UK and sunny California, USA - a time difference of 8 hours. As my day finished, her day was just beginning, requiring us to be deliberate about maximising the time we could spend with each other during our waking hours, and figuring out ways for us to communicate effectively when we didn&rsquo;t cross over - voice notes, letters, email and so on. Leaving little things for her to wake up to.</p>
<p>Operating a remote team is a lot like that! Especially with the time zone differences I&rsquo;ve had to overcome. There are a few key tenets, including using the <a href="/post/tools-for-a-culture-of-writing">tools for a culture of writing</a>.</p>
<h3 id="there-is-no-communication-like-overcommunication">There Is No Communication Like Overcommunication</h3>
<p>The old adage is to <a href="https://medium.com/unexpected-leadership/say-it-7-times-the-art-of-overcommunication-5d019b2c33d4">say something several times before it is truly heard</a>, and the goes doubly for remote teams. Say something in a meeting? That meeting better be recorded, transcribed, summarised (see <a href="https://en.wikipedia.org/wiki/BLUF_(communication)">bottom line, up front</a>) and the key points posted in your collaboration tool of choice. LLM-based tools can help a lot with this, but it is still your responsibility to ensure the summary is accurate and useful - I personally prefer to write my executive summaries by hand.</p>
<p>Why do this? Team members may be unable to make a given meeting, either temporarily or permanently, but no one should ever be out of the loop. Some folks may consider holding &ldquo;off the record&rdquo; meetings where no recording takes place - in those cases, take notes, respecting the speaker(s) by following the <a href="https://en.wikipedia.org/wiki/Chatham_House_Rule">Chatham House Rule</a> and/or being judicious about omitting information from notes, but share it with team members who could not make it verbally. Don&rsquo;t let information fall into a hole.</p>
<p>As a result of the timezone disparities, the team has opted for a written-first, asynchronous-first approach to information sharing. Daily standups are written, most projects are driven through task trackers, documents and IM, with sparing use of synchronous meetings - usually for our retrospectives and weekly team meetings to ensure we have some regular face time.</p>
<p>On that note, we strive to have regular in-person off-sites. These are partially structured, to allow for high value synchronous collaboration, but also leave some unstructured time to allow the team to &ldquo;be people&rdquo; together.</p>
<h2 id="the-team-is-the-unit-of-delivery">The Team Is The Unit Of Delivery</h2>
<p>I think I first heard this term back in 2014, maybe 2015? It comes from the UK&rsquo;s <a href="https://gds.blog.gov.uk/">Government Digital Service</a>, and has been the way I feel about teams ever since.</p>
<figure><img src="/long-distance-relationships/team.webp"><figcaption>
      <h4>The Unit Of Delivery Is The Team. Government Digital Service.</h4>
    </figcaption>
</figure>

<p>What does this mean? Through individual performance and collaboration, we deliver value to our customers. &ldquo;The team&rdquo; transcends my reports into our wider organisation and the cross-functional relationships we build.</p>
<h3 id="what-about-the-individual">What About The Individual?</h3>
<p>This is not to say I don&rsquo;t value the individual - we must recognise that the team is successful because of individual skillsets and ability, and that we cover each other&rsquo;s bases. Shipping a product as complex as ours is simply not possible by an individual. I generally recommend that my staff maintain a <a href="https://jvns.ca/blog/brag-documents/">brag doc</a>, and I track their individual contributions within the context of providing feedback and performance management.</p>
<p>Performance management aside, I try and avoid focusing on &ldquo;individual&rdquo; failures - we succeed and fail as one unit.</p>
<h2 id="session-musicians-not-rockstars">Session Musicians, Not Rockstars</h2>
<p>Assembling a team that can function as one unit and be healthy is tricky, especially in a remote environment - there can be interactions that you are not privy to via DMs, private channels and so on. As a result, there must be a high level of trust between each team member, as well as the staff and the management layer. It is a lot like constructing a high performing sports team, and the relationship between the front office and the back office.</p>
<p>I&rsquo;m often heard saying that I&rsquo;ve learned more from the great sports coaches of our time (<a href="https://en.wikipedia.org/wiki/Phil_Jackson">Phil Jackson</a>, <a href="https://en.wikipedia.org/wiki/Nick_Saban">Nick Saban</a>, <a href="https://en.wikipedia.org/wiki/Pep_Guardiola">Pep Guardiola</a>) than most management trainings and material, and part of this is construction of a strong team. I don&rsquo;t enjoy managing egos, so intentionally select for folks who display high levels of coachability and curiosity with the right amount of appropriate professional drive.</p>
<p>As an example of this practice, consider session musicians - consummate professionals quietly behind some of the most influential music produced in the 20th and 21st centuries. A great example of this were <a href="https://en.wikipedia.org/wiki/Muscle_Shoals_Rhythm_Section">the Swampers</a>, with Rick Hall conjouring a kind of magic rarely seen before or since.</p>
<p>Sure, rockstars are great for the spark of inguinity and allowing lightning to strike, but software engineering is a long game, and niche of managed database services and database service reliability engineering even more so. I need the longevity of a good session group.</p>
<h2 id="work-is-fungible-team-members-are-not">Work Is Fungible, Team Members Are Not</h2>
<p>Along the lines of longevity, one of the more painful lessons learned is that work is fungible, team members are not.</p>
<p>In short, teams can learn (as a function of time and funding) pretty much anything you need them to, so moving work to different teams is &ldquo;free&rdquo;. However, breaking that team up to align closer to the work is <em>extremely</em> expensive.</p>
<p>The thesis is that, as a team is one unit, breaking that unit creates a new team. Think of a team as a graph of nodes and edges - when you pop a node from the graph, the graph has changed. Adding a new node or nodes to the graph? A new graph.</p>
<p>Thinking this through even more, a software engineering team is generally in a <a href="https://en.wikipedia.org/wiki/Metastability">metastable</a> state. There is <em>probably</em> a theoretic stable state of extremely strong bonds between individuals, but in real life this is rarely, if ever, achieved - even the very long term session groups or sports teams eventually break up due to internal or external forces.</p>
<p>We want to maximise the stability of the team, and to do that, we bring the work to the team, rather than take the team members to the work. As an example, a team that ostensibly works on an integration product picked up work to enable billing for new regions. The originally proposed option was to &ldquo;air drop&rdquo; members from that team into the billing the team, but doing so would have unacceptably disrupted the effectiveness of the original team. Moving the work has much lower cost.</p>
<h2 id="the-pr-machine">The PR Machine</h2>
<p>Last but not least, one of the key lessons I&rsquo;ve learned in remote leadership has been that as all interactions are intentional, you have to be the PR machine for your team. This loops back around to overcommunication - if a major project is shipped and no one hears about it, did it even happen?</p>
<p>Your job is to take your team&rsquo;s accolades and broadcast them wide and far. Have your team be known as one that Gets Stuff Done. Have your team be known as one that is a benchmark for quality and reliability.</p>
<p>A lot of this work is a continuation of generating artefacts for your teams work, and using those artefacts as part of your PR campaign towards other teams and upper leadership. This is not entirely altruistic - a successful, high profile team reflects well on you as a manager, furthering your career.</p>
<p><em>A previous version of this article used an AI-generated header image. This has been replaced with something hand drawn.</em></p>]]></content:encoded></item><item><title>Modifying pg_dump To Exclude Event Triggers</title><link>https://matt.blwt.io/post/modifying-pgdump/</link><pubDate>Sun, 30 Jun 2024 20:50:47 +0100</pubDate><guid>https://matt.blwt.io/post/modifying-pgdump/</guid><description>&lt;p&gt;At &lt;code&gt;$WORK&lt;/code&gt;, we have a case where we have implemented an event trigger to prevent customers from dropping an extension. As this extension is part of &lt;code&gt;contrib&lt;/code&gt; and normally installed by users, we can&amp;rsquo;t prevent them from dropping it normally. However, event triggers can only be created by superusers, so a &lt;code&gt;pg_dump&lt;/code&gt; of the database creates a dump that can&amp;rsquo;t be restored by a non-superuser. To solve this, lets implement a custom &lt;code&gt;pg_dump&lt;/code&gt; that optionally excludes event triggers.&lt;/p&gt;
&lt;p&gt;You can see the following implementation in &lt;a href="https://github.com/mble/postgres/compare/REL_16_STABLE...mble:postgres:mble-filter-event-triggers"&gt;this diff&lt;/a&gt; on my fork of the Postgres repository.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>At <code>$WORK</code>, we have a case where we have implemented an event trigger to prevent customers from dropping an extension. As this extension is part of <code>contrib</code> and normally installed by users, we can&rsquo;t prevent them from dropping it normally. However, event triggers can only be created by superusers, so a <code>pg_dump</code> of the database creates a dump that can&rsquo;t be restored by a non-superuser. To solve this, lets implement a custom <code>pg_dump</code> that optionally excludes event triggers.</p>
<p>You can see the following implementation in <a href="https://github.com/mble/postgres/compare/REL_16_STABLE...mble:postgres:mble-filter-event-triggers">this diff</a> on my fork of the Postgres repository.</p>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#adding-a-new-option">Adding A New Option</a></li>
<li><a href="#filtering-event-triggers">Filtering Event Triggers</a></li>
<li><a href="#filtering-event-trigger-returning-functions">Filtering Event Trigger Returning Functions</a></li>
<li><a href="#testing">Testing</a></li>
</ul>
<h2 id="adding-a-new-option">Adding A New Option</h2>
<p>To begin with, we need to add a new switch to <code>pg_dump</code> that will allow us to exclude event triggers on command. The switches generally come in two varieties - short and long. As <code>pg_dump</code> already uses most of the alphabet in the short options (<code>abBcCdeEfFhjnNOpRsStTUvwWxZ</code>), we&rsquo;ll use a long option: <code>--no-event-triggers</code>. This aligns with similar options in <code>pg_dump</code> like <code>--no-comments</code> and <code>--no-subscriptions</code>.</p>
<p>This is fortunately straightforward to implement - in <code>pg_backup.h</code> we add a new boolean variable to the <code>_dumpOptions</code> struct:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="n">_dumpOptions</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">ConnParams</span> <span class="n">cparams</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* ... */</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="cm">/* flags for various command-line long options */</span>
</span></span><span class="line"><span class="cl">	<span class="kt">int</span>			<span class="n">disable_dollar_quoting</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">	<span class="kt">int</span>			<span class="n">column_inserts</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">	<span class="kt">int</span>			<span class="n">if_exists</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* ... */</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int</span>			<span class="n">outputNoEventTriggers</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* ... */</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="n">DumpOptions</span><span class="p">;</span>
</span></span></code></pre></div><p>In <code>pg_dump.c</code>, we add the option to the <code>long_options</code> array:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="k">struct</span> <span class="n">option</span> <span class="n">long_options</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* ... */</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s">&#34;no-table-access-method&#34;</span><span class="p">,</span> <span class="n">no_argument</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">dopt</span><span class="p">.</span><span class="n">outputNoTableAm</span><span class="p">,</span> <span class="mi">1</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s">&#34;no-tablespaces&#34;</span><span class="p">,</span> <span class="n">no_argument</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">dopt</span><span class="p">.</span><span class="n">outputNoTablespaces</span><span class="p">,</span> <span class="mi">1</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="s">&#34;no-event-triggers&#34;</span><span class="p">,</span> <span class="n">no_argument</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">dopt</span><span class="p">.</span><span class="n">outputNoEventTriggers</span><span class="p">,</span> <span class="mi">1</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* ... */</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><p>And finally, we add the option to the <code>help</code> output:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">void</span>
</span></span><span class="line"><span class="cl"><span class="nf">help</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">progname</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="nf">_</span><span class="p">(</span><span class="s">&#34;%s dumps a database as a text file or to other formats.</span><span class="se">\n\n</span><span class="s">&#34;</span><span class="p">),</span> <span class="n">progname</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* ... */</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="nf">_</span><span class="p">(</span><span class="s">&#34;  --no-event-triggers          do not dump event triggers</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>With all that done, we now have a dump option we can use to filter out event triggers.</p>
<h2 id="filtering-event-triggers">Filtering Event Triggers</h2>
<p>The next step is to actually filter out the event triggers from the dump if the option is set. The entrypoint for this is the <code>getEventTriggers</code> function in <code>pg_dump.c</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/*
</span></span></span><span class="line"><span class="cl"><span class="cm"> * getEventTriggers
</span></span></span><span class="line"><span class="cl"><span class="cm"> *	  get information about event triggers
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="n">EventTriggerInfo</span> <span class="o">*</span>
</span></span><span class="line"><span class="cl"><span class="nf">getEventTriggers</span><span class="p">(</span><span class="n">Archive</span> <span class="o">*</span><span class="n">fout</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="n">numEventTriggers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* ... */</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">evtinfo</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>As this function takes an <code>Archive</code> pointer as an argument, we can access the dump options from it. We can then check if the <code>outputNoEventTriggers</code> option is set and skip the event triggers if it is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">EventTriggerInfo</span> <span class="o">*</span>
</span></span><span class="line"><span class="cl"><span class="nf">getEventTriggers</span><span class="p">(</span><span class="n">Archive</span> <span class="o">*</span><span class="n">fout</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="n">numEventTriggers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* ... */</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">fout</span><span class="o">-&gt;</span><span class="n">dopt</span><span class="o">-&gt;</span><span class="n">outputNoEventTriggers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">pg_log_info</span><span class="p">(</span><span class="s">&#34;excluded event triggers&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="o">*</span><span class="n">numEventTriggers</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">pg_log_info</span><span class="p">(</span><span class="s">&#34;reading event triggers&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* ... */</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">evtinfo</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>With this change, we can now exclude event triggers from the dump if the <code>--no-event-triggers</code> option is set. However, this results in a somewhat incomplete dump - we have some functions references by these triggers left hanging, so we&rsquo;ll also need to exclude them.</p>
<h2 id="filtering-event-trigger-returning-functions">Filtering Event Trigger Returning Functions</h2>
<p>There are a few ways we can identify the functions that are used to return event triggers. The most straightforward is to look for functions that have the <code>EVENT TRIGGER</code> type:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">p</span><span class="p">.</span><span class="n">oid</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">p</span><span class="p">.</span><span class="n">proname</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">pg_proc</span><span class="w"> </span><span class="n">p</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">p</span><span class="p">.</span><span class="n">prorettype</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;pg_catalog.event_trigger&#39;</span><span class="p">::</span><span class="n">regtype</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>Another option would be to use <code>pg_depend</code> to find functions that are depended on by event triggers:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">pg_proc</span><span class="p">.</span><span class="n">oid</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">function_oid</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">pg_proc</span><span class="p">.</span><span class="n">proname</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">function_name</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">pg_proc</span><span class="p">.</span><span class="n">pronamespace</span><span class="p">::</span><span class="n">regnamespace</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">function_schema</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">pg_proc</span><span class="p">.</span><span class="n">proowner</span><span class="p">::</span><span class="n">regrole</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">function_owner</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">pg_event_trigger</span><span class="p">.</span><span class="n">oid</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">trigger_oid</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">pg_event_trigger</span><span class="p">.</span><span class="n">evtname</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="k">trigger_name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">pg_depend</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">INNER</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">pg_event_trigger</span><span class="w"> </span><span class="k">ON</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="n">pg_event_trigger</span><span class="p">.</span><span class="n">oid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">pg_depend</span><span class="p">.</span><span class="n">objid</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">			</span><span class="k">AND</span><span class="w"> </span><span class="n">pg_depend</span><span class="p">.</span><span class="n">classid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;pg_event_trigger&#39;</span><span class="p">::</span><span class="n">regclass</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">INNER</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">pg_proc</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">pg_depend</span><span class="p">.</span><span class="n">refobjid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">pg_proc</span><span class="p">.</span><span class="n">oid</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">		</span><span class="k">AND</span><span class="w"> </span><span class="n">pg_depend</span><span class="p">.</span><span class="n">refclassid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;pg_proc&#39;</span><span class="p">::</span><span class="n">regclass</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">pg_depend</span><span class="p">.</span><span class="n">deptype</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;n&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>In the interests of simplicity, we&rsquo;ll use the first method. There is a reasonably complex function, <code>getFuncs</code>, that is used to get all functions in the database, that aren&rsquo;t aggregates, internal constructor funcs, and a couple of other special cases (e.g. binary-upgrade mode includes extension-managed functions). We can add a check for the <code>pg_catalog.event_trigger</code> return type to this query:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">FuncInfo</span> <span class="o">*</span>
</span></span><span class="line"><span class="cl"><span class="nf">getFuncs</span><span class="p">(</span><span class="n">Archive</span> <span class="o">*</span><span class="n">fout</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="n">numFuncs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* ... */</span>
</span></span><span class="line"><span class="cl">    <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">not_event_trigger_check</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">not_event_trigger_check</span> <span class="o">=</span> <span class="p">(</span><span class="n">fout</span><span class="o">-&gt;</span><span class="n">dopt</span><span class="o">-&gt;</span><span class="n">outputNoEventTriggers</span> <span class="o">?</span> <span class="s">&#34;</span><span class="se">\n</span><span class="s">AND p.prorettype &lt;&gt; &#39;pg_catalog.event_trigger&#39;::regtype</span><span class="se">\n</span><span class="s">&#34;</span> <span class="o">:</span> <span class="s">&#34;</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="cm">/* ... */</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">		<span class="nf">appendPQExpBuffer</span><span class="p">(</span><span class="n">query</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;SELECT p.tableoid, p.oid, p.proname, p.prolang, &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;p.pronargs, p.proargtypes, p.prorettype, &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;p.proacl, &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;acldefault(&#39;f&#39;, p.proowner) AS acldefault, &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;p.pronamespace, &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;p.proowner &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;FROM pg_proc p &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;LEFT JOIN pg_init_privs pip ON &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;(p.oid = pip.objoid &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;AND pip.classoid = &#39;pg_proc&#39;::regclass &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;AND pip.objsubid = 0) &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;WHERE %s&#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;%s&#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;</span><span class="se">\n</span><span class="s">  AND NOT EXISTS (SELECT 1 FROM pg_depend &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;WHERE classid = &#39;pg_proc&#39;::regclass AND &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;objid = p.oid AND deptype = &#39;i&#39;)&#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;</span><span class="se">\n</span><span class="s">  AND (&#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;</span><span class="se">\n</span><span class="s">  pronamespace != &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;(SELECT oid FROM pg_namespace &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;WHERE nspname = &#39;pg_catalog&#39;)&#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;</span><span class="se">\n</span><span class="s">  OR EXISTS (SELECT 1 FROM pg_cast&#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;</span><span class="se">\n</span><span class="s">  WHERE pg_cast.oid &gt; %u &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;</span><span class="se">\n</span><span class="s">  AND p.oid = pg_cast.castfunc)&#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;</span><span class="se">\n</span><span class="s">  OR EXISTS (SELECT 1 FROM pg_transform&#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;</span><span class="se">\n</span><span class="s">  WHERE pg_transform.oid &gt; %u AND &#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;</span><span class="se">\n</span><span class="s">  (p.oid = pg_transform.trffromsql&#34;</span>
</span></span><span class="line"><span class="cl">						  <span class="s">&#34;</span><span class="se">\n</span><span class="s">  OR p.oid = pg_transform.trftosql))&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">						  <span class="n">not_agg_check</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">						  <span class="n">not_event_trigger_check</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">						  <span class="n">g_last_builtin_oid</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">						  <span class="n">g_last_builtin_oid</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="cm">/* ... */</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">finfo</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This is a little ugly, but gets the job done without having to add a lot of additional complexity. This doesn&rsquo;t correctly handle all cases - much older versions of Postgres won&rsquo;t do the filtering correctly, but at <code>$WORK</code> we don&rsquo;t have anything older than 12, so this is sufficient for our needs. Should I want this to be upstreamed, I&rsquo;ll need to do some more robust testing on older versions.</p>
<h2 id="testing">Testing</h2>
<p>With all this done, we can now test our changes. We can build <code>pg_dump</code> as normal, and then run it with the <code>--no-event-triggers</code> option:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ ~/dev/mble/pgsql/bin/pg_dump --quote-all-identifiers -O -s -v 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">|</span> rg <span class="s1">&#39;(CREATE EVENT TRIGGER|(FUNCTION.* RETURNS &#34;event_trigger&#34;))&#39;</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">&#34;No event triggers found&#34;</span>
</span></span><span class="line"><span class="cl">CREATE FUNCTION <span class="s2">&#34;public&#34;</span>.<span class="s2">&#34;event_trigger_function_name&#34;</span><span class="o">()</span> RETURNS <span class="s2">&#34;event_trigger&#34;</span>
</span></span><span class="line"><span class="cl">CREATE EVENT TRIGGER <span class="s2">&#34;trigger_name&#34;</span> ON <span class="s2">&#34;ddl_command_start&#34;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ ~/dev/mble/pgsql/bin/pg_dump --no-event-triggers --quote-all-identifiers -O -s -v 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">|</span> rg <span class="s1">&#39;(CREATE EVENT TRIGGER|(FUNCTION.* RETURNS &#34;event_trigger&#34;))&#39;</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">&#34;No event triggers found&#34;</span>
</span></span><span class="line"><span class="cl">No event triggers found
</span></span></code></pre></div><p>Postgres has a fairly robust TAP testing suite for <code>pg_dump</code> and other binary utilities, where they are invoked by Perl scripts using <a href="https://metacpan.org/dist/IPC-Run"><code>IPC::Run</code></a>. We can run these tests by running <code>make check</code> in the <code>src/bin/pg_dump</code> directory:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ <span class="nb">cd</span> src/bin/pg_dump/
</span></span><span class="line"><span class="cl">$ make check
</span></span><span class="line"><span class="cl"><span class="c1"># snip</span>
</span></span><span class="line"><span class="cl"><span class="c1"># +++ tap check in src/bin/pg_dump +++</span>
</span></span><span class="line"><span class="cl">t/001_basic.pl ................ ok
</span></span><span class="line"><span class="cl">t/002_pg_dump.pl .............. ok
</span></span><span class="line"><span class="cl">t/003_pg_dump_with_server.pl .. ok
</span></span><span class="line"><span class="cl">t/004_pg_dump_parallel.pl ..... ok
</span></span><span class="line"><span class="cl">t/010_dump_connstr.pl ......... ok
</span></span><span class="line"><span class="cl">All tests successful.
</span></span><span class="line"><span class="cl"><span class="nv">Files</span><span class="o">=</span>5, <span class="nv">Tests</span><span class="o">=</span>10018, <span class="m">19</span> wallclock secs <span class="o">(</span> 0.23 usr  0.03 sys +  3.31 cusr  2.78 <span class="nv">csys</span> <span class="o">=</span>  6.35 CPU<span class="o">)</span>
</span></span><span class="line"><span class="cl">Result: PASS
</span></span></code></pre></div><p>To assert that our new option is working correctly, we can add a new test to the <code>t/002_pg_dump.pl</code> script:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-perl" data-lang="perl"><span class="line"><span class="cl"><span class="k">my</span> <span class="nv">%pgdump_runs</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># snip</span>
</span></span><span class="line"><span class="cl">	<span class="n">no_event_triggers</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="n">dump_cmd</span> <span class="o">=&gt;</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">			<span class="s">&#39;pg_dump&#39;</span><span class="p">,</span> <span class="s">&#39;--no-sync&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">			<span class="s">&#34;--file=$tempdir/no_event_triggers.sql&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">			<span class="s">&#39;--no-event-triggers&#39;</span><span class="p">,</span> <span class="s">&#39;postgres&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="p">]</span>
</span></span><span class="line"><span class="cl">	<span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># snip</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-perl" data-lang="perl"><span class="line"><span class="cl"><span class="k">my</span> <span class="nv">%full_runs</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># snip</span>
</span></span><span class="line"><span class="cl">    <span class="n">no_event_triggers</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>We can now run the tests and observe they fail, after adding <code>no_event_triggers</code> to the <code>full_runs</code> hash:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ make check
</span></span><span class="line"><span class="cl"><span class="c1"># snip</span>
</span></span><span class="line"><span class="cl"><span class="c1"># +++ tap check in src/bin/pg_dump +++</span>
</span></span><span class="line"><span class="cl">t/001_basic.pl ................ ok
</span></span><span class="line"><span class="cl">t/002_pg_dump.pl .............. 4387/?
</span></span><span class="line"><span class="cl"><span class="c1">#   Failed test &#39;no_event_triggers: should dump CREATE EVENT TRIGGER test_event_trigger&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   at t/002_pg_dump.pl line 4960.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Review no_event_triggers results in /Users/mblewitt/dev/mble/postgres/src/bin/pg_dump/tmp_check/tmp_test_XEVK</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#   Failed test &#39;no_event_triggers: should dump CREATE FUNCTION dump_test.event_trigger_func&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   at t/002_pg_dump.pl line 4960.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Review no_event_triggers results in /Users/mblewitt/dev/mble/postgres/src/bin/pg_dump/tmp_check/tmp_test_XEVK</span>
</span></span><span class="line"><span class="cl">t/002_pg_dump.pl .............. 9428/? <span class="c1"># Looks like you failed 2 tests of 9907.</span>
</span></span><span class="line"><span class="cl">t/002_pg_dump.pl .............. Dubious, <span class="nb">test</span> returned <span class="m">2</span> <span class="o">(</span>wstat 512, 0x200<span class="o">)</span>
</span></span><span class="line"><span class="cl">Failed 2/9907 subtests
</span></span><span class="line"><span class="cl">t/003_pg_dump_with_server.pl .. ok
</span></span><span class="line"><span class="cl">t/004_pg_dump_parallel.pl ..... ok
</span></span><span class="line"><span class="cl">t/010_dump_connstr.pl ......... ok
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Test Summary Report
</span></span><span class="line"><span class="cl">-------------------
</span></span><span class="line"><span class="cl">t/002_pg_dump.pl            <span class="o">(</span>Wstat: <span class="m">512</span> <span class="o">(</span>exited 2<span class="o">)</span> Tests: <span class="m">9907</span> Failed: 2<span class="o">)</span>
</span></span><span class="line"><span class="cl">  Failed tests:  4964, <span class="m">4969</span>
</span></span><span class="line"><span class="cl">  Non-zero <span class="nb">exit</span> status: <span class="m">2</span>
</span></span><span class="line"><span class="cl"><span class="nv">Files</span><span class="o">=</span>5, <span class="nv">Tests</span><span class="o">=</span>10018, <span class="m">16</span> wallclock secs <span class="o">(</span> 0.19 usr  0.03 sys +  3.03 cusr  2.38 <span class="nv">csys</span> <span class="o">=</span>  5.63 CPU<span class="o">)</span>
</span></span><span class="line"><span class="cl">Result: FAIL
</span></span></code></pre></div><p>We can update our expectations in the test script by adding the expected output to the <code>%tests</code> hash:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-perl" data-lang="perl"><span class="line"><span class="cl"><span class="k">my</span> <span class="nv">%tests</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># snip</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#39;CREATE FUNCTION dump_test.event_trigger_func&#39;</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="n">create_order</span> <span class="o">=&gt;</span> <span class="mi">32</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="n">create_sql</span> <span class="o">=&gt;</span> <span class="s">&#39;CREATE FUNCTION dump_test.event_trigger_func()
</span></span></span><span class="line"><span class="cl"><span class="s">					   RETURNS event_trigger LANGUAGE plpgsql
</span></span></span><span class="line"><span class="cl"><span class="s">					   AS $$ BEGIN RETURN; END;$$;&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="n">regexp</span> <span class="o">=&gt;</span> <span class="sx">qr/^
</span></span></span><span class="line"><span class="cl"><span class="sx">			\QCREATE FUNCTION dump_test.event_trigger_func() RETURNS event_trigger\E
</span></span></span><span class="line"><span class="cl"><span class="sx">			\n\s+\QLANGUAGE plpgsql\E
</span></span></span><span class="line"><span class="cl"><span class="sx">			\n\s+AS\ \$\$
</span></span></span><span class="line"><span class="cl"><span class="sx">			\Q BEGIN RETURN; END;\E
</span></span></span><span class="line"><span class="cl"><span class="sx">			\$\$;/</span><span class="n">xm</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="n">like</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="cl">		  <span class="p">{</span> <span class="nv">%full_runs</span><span class="p">,</span> <span class="nv">%dump_test_schema_runs</span><span class="p">,</span> <span class="n">section_pre_data</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">		<span class="n">unlike</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="n">exclude_dump_test_schema</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">			<span class="n">only_dump_measurement</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">			<span class="n">no_event_triggers</span> <span class="o">=&gt;</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">		<span class="p">},</span>
</span></span><span class="line"><span class="cl">	<span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># snip</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#39;CREATE EVENT TRIGGER test_event_trigger&#39;</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="n">create_order</span> <span class="o">=&gt;</span> <span class="mi">33</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="n">create_sql</span> <span class="o">=&gt;</span> <span class="s">&#39;CREATE EVENT TRIGGER test_event_trigger
</span></span></span><span class="line"><span class="cl"><span class="s">					   ON ddl_command_start
</span></span></span><span class="line"><span class="cl"><span class="s">					   EXECUTE FUNCTION dump_test.event_trigger_func();&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="n">regexp</span> <span class="o">=&gt;</span> <span class="sx">qr/^
</span></span></span><span class="line"><span class="cl"><span class="sx">			\QCREATE EVENT TRIGGER test_event_trigger \E
</span></span></span><span class="line"><span class="cl"><span class="sx">			\QON ddl_command_start\E
</span></span></span><span class="line"><span class="cl"><span class="sx">			\n\s+\QEXECUTE FUNCTION dump_test.event_trigger_func();\E
</span></span></span><span class="line"><span class="cl"><span class="sx">			/</span><span class="n">xm</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="n">like</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nv">%full_runs</span><span class="p">,</span> <span class="n">section_post_data</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">		<span class="n">unlike</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="n">no_event_triggers</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">		<span class="p">},</span>
</span></span><span class="line"><span class="cl">	<span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></div><p>The crucial part of this is the <code>no_event_triggers</code> key in the <code>unlike</code> hash. As we don&rsquo;t expect the <code>create_sql</code> to be present in the dump, we can assert that it isn&rsquo;t present by adding this key.</p>
<p>With this change, we can now run the tests again and see that they pass:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ <span class="nb">cd</span> src/bin/pg_dump/
</span></span><span class="line"><span class="cl">$ make check
</span></span><span class="line"><span class="cl"><span class="c1"># snip</span>
</span></span><span class="line"><span class="cl"><span class="c1"># +++ tap check in src/bin/pg_dump +++</span>
</span></span><span class="line"><span class="cl">t/001_basic.pl ................ ok
</span></span><span class="line"><span class="cl">t/002_pg_dump.pl .............. ok
</span></span><span class="line"><span class="cl">t/003_pg_dump_with_server.pl .. ok
</span></span><span class="line"><span class="cl">t/004_pg_dump_parallel.pl ..... ok
</span></span><span class="line"><span class="cl">t/010_dump_connstr.pl ......... ok
</span></span><span class="line"><span class="cl">All tests successful.
</span></span><span class="line"><span class="cl"><span class="nv">Files</span><span class="o">=</span>5, <span class="nv">Tests</span><span class="o">=</span>10018, <span class="m">19</span> wallclock secs <span class="o">(</span> 0.23 usr  0.03 sys +  3.31 cusr  2.78 <span class="nv">csys</span> <span class="o">=</span>  6.35 CPU<span class="o">)</span>
</span></span><span class="line"><span class="cl">Result: PASS
</span></span></code></pre></div><p>With this, we have successfully implemented a new option in <code>pg_dump</code> to exclude event triggers from the dump. This will allow us to create dumps that can be restored by non-superusers, even if event triggers are present in the database.</p>]]></content:encoded></item><item><title>Building a PostgreSQL Extension, Line by Line</title><link>https://matt.blwt.io/post/building-a-postgresql-extension-line-by-line/</link><pubDate>Fri, 14 Jun 2024 19:06:31 +0100</pubDate><guid>https://matt.blwt.io/post/building-a-postgresql-extension-line-by-line/</guid><description>&lt;p&gt;I&amp;rsquo;ve been working on a particular problem - how to offer logical replication for a very large number of Postgres databases, where I don&amp;rsquo;t have the ability (or capacity) to liaise with the users of the databases one-to-one. In order to mitigate some of the &lt;a href="https://matt.blwt.io/post/logical-replication-guardrails"&gt;limitations&lt;/a&gt;, I wanted to implement a flexible extension to help guard against two of the larger pitfalls - DDL and large objects.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>I&rsquo;ve been working on a particular problem - how to offer logical replication for a very large number of Postgres databases, where I don&rsquo;t have the ability (or capacity) to liaise with the users of the databases one-to-one. In order to mitigate some of the <a href="/post/logical-replication-guardrails">limitations</a>, I wanted to implement a flexible extension to help guard against two of the larger pitfalls - DDL and large objects.</p>
<p>There are some other interesting problems here, including enforcing primary keys on all tables and ensuring <code>REPLICA IDENTITY FULL</code> is set for all tables. For now, I&rsquo;m going to focus on building an extension for DDL and large object guardrails. Extensions are probably my favourite Postgres feature.</p>
<h2 id="contents">Contents</h2>
<ul>
<li><a href="#extension-overview">Extension Overview</a></li>
<li><a href="#extension-implementation">Extension Implementation</a>
<ul>
<li><a href="#control-file">Control File</a></li>
<li><a href="#makefile">Makefile</a></li>
<li><a href="#install-sql">Install SQL</a></li>
<li><a href="#c-extension">C Extension</a></li>
<li><a href="#tests">Tests</a></li>
</ul>
</li>
<li><a href="#considerations">Considerations</a></li>
<li><a href="#putting-it-all-together">Putting It All Together</a></li>
</ul>
<h2 id="extension-overview">Extension Overview</h2>
<p>The extension, <a href="https://github.com/mble/ddl_guard"><code>ddl_guard</code></a>, has the following features:</p>
<ul>
<li>Can be enabled or disabled by superusers, through a GUC.</li>
<li>Can prevent DDL commands from being executed by non-superusers.</li>
<li>Can operate in &ldquo;sentinel mode&rdquo;, where instead of preventing execution, a file is written to disk to indicate that a DDL command has been run.</li>
<li>Can operate in &ldquo;sentinel mode&rdquo; for the large object facility.</li>
</ul>
<p>The &ldquo;sentinel mode&rdquo; was mostly inspired by AWS&rsquo; <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments-overview.html#blue-green-deployments-limitations-postgres">RDS Blue/Green deployments</a>, where a notification is sent to the customer if they have performed an incompatible action.</p>
<p>Here is a quick <code>tree</code> of the directory structure:</p>
<pre tabindex="0"><code>.
├── LICENSE
├── Makefile
├── README.md
├── bin
│   └── test
├── ddl_guard--1.0.0.sql
├── ddl_guard.c
├── ddl_guard.control
└── test
    ├── data
    │   └── test.data
    ├── ddl_guard.conf
    ├── regular
    │   ├── expected
    │   │   ├── ddl.out
    │   │   └── lobject.out
    │   └── sql
    │       ├── ddl.sql
    │       └── lobject.sql
    └── superuser
        ├── expected
        │   ├── ddl.out
        │   └── lobject.out
        └── sql
            ├── ddl.sql
            └── lobject.sql
</code></pre><h2 id="extension-implementation">Extension Implementation</h2>
<h3 id="control-file">Control File</h3>
<p>When building an extension, particularly in C, generally the first thing to do is create a <code>control</code> file. This file is used by Postgres to understand the extension and how to load it. More on the definitions here: <a href="https://www.postgresql.org/docs/16/extend-extensions.html#EXTEND-EXTENSIONS-FILES">Extension Files</a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># ddl_guard.control</span>
</span></span><span class="line"><span class="cl"><span class="c1"># The version of the extension. Semver is not enforced, but good to follow.</span>
</span></span><span class="line"><span class="cl"><span class="na">default_version</span> <span class="o">=</span> <span class="s">&#39;1.0.0&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Initially applied on first install.</span>
</span></span><span class="line"><span class="cl"><span class="na">comment</span> <span class="o">=</span> <span class="s">&#39;Prevents DDL execution by non-superusers when a specific GUC is set&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Prevent or allow moving the extension and associated objects</span>
</span></span><span class="line"><span class="cl"><span class="c1"># to another schema after install.</span>
</span></span><span class="line"><span class="cl"><span class="na">relocatable</span> <span class="o">=</span> <span class="s">false</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Used when invoking &#39;MODULE_PATHNAME&#39; when installing C extensions.</span>
</span></span><span class="line"><span class="cl"><span class="na">module_pathname</span> <span class="o">=</span> <span class="s">&#39;$libdir/ddl_guard&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Forces the extension to be installable only by superusers.</span>
</span></span><span class="line"><span class="cl"><span class="na">superuser</span> <span class="o">=</span> <span class="s">true</span>
</span></span></code></pre></div><h3 id="makefile">Makefile</h3>
<p>Next, we create a <code>Makefile</code> to compile the extension. <code>ddl_guard</code> has a very simple <code>Makefile</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Makefile" data-lang="Makefile"><span class="line"><span class="cl"><span class="nv">MODULES</span> <span class="o">=</span> ddl_guard <span class="c1"># Name of the built module.</span>
</span></span><span class="line"><span class="cl"><span class="nv">EXTENSION</span> <span class="o">=</span> ddl_guard <span class="c1"># Name of the extension.</span>
</span></span><span class="line"><span class="cl"><span class="nv">DATA</span> <span class="o">=</span> ddl_guard--1.0.0.sql <span class="c1"># The install SQL script.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># The directory containing the test files.
</span></span></span><span class="line"><span class="cl"><span class="c"># Can be overidden by setting the INPUTDIR environment variable.
</span></span></span><span class="line"><span class="cl"><span class="c"></span><span class="nv">INPUTDIR</span> <span class="o">?=</span> test/regular
</span></span><span class="line"><span class="cl"><span class="c"># The test files. Uses Makefile wildcard expansion.
</span></span></span><span class="line"><span class="cl"><span class="c"></span><span class="nv">TESTS</span> <span class="o">=</span> <span class="k">$(</span>wildcard <span class="k">$(</span>INPUTDIR<span class="k">)</span>/sql/*.sql<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="c"># The test names. Uses Makefile pattern substitution
</span></span></span><span class="line"><span class="cl"><span class="c"># to extract the test suite names.
</span></span></span><span class="line"><span class="cl"><span class="c"></span><span class="nv">REGRESS</span> <span class="o">=</span> <span class="k">$(</span>patsubst <span class="k">$(</span>INPUTDIR<span class="k">)</span>/sql/%.sql,%,<span class="k">$(</span>TESTS<span class="k">))</span>
</span></span><span class="line"><span class="cl"><span class="c"># The test options.
</span></span></span><span class="line"><span class="cl"><span class="c"># --inputdir: The directory containing the test files.
</span></span></span><span class="line"><span class="cl"><span class="c"># --load-extension: The extension to load as part of the test run.
</span></span></span><span class="line"><span class="cl"><span class="c"># --temp-config: The temporary configuration file to use for the test.
</span></span></span><span class="line"><span class="cl"><span class="c"># --use-existing: Use an existing database for the test.
</span></span></span><span class="line"><span class="cl"><span class="c"># See: https://www.postgresql.org/docs/current/extend-pgxs.html#EXTEND-PGXS
</span></span></span><span class="line"><span class="cl"><span class="c"></span><span class="nv">REGRESS_OPTS</span> <span class="o">=</span> --inputdir<span class="o">=</span><span class="k">$(</span>INPUTDIR<span class="k">)</span> --load-extension<span class="o">=</span><span class="k">$(</span>EXTENSION<span class="k">)</span> --temp-config<span class="o">=</span>test/ddl_guard.conf --use-existing
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Set a compiler flag when built under different architectures.
</span></span></span><span class="line"><span class="cl"><span class="c"></span><span class="nv">OPTFLAGS</span> <span class="o">=</span> -march<span class="o">=</span>native
</span></span><span class="line"><span class="cl"><span class="err">ifeq</span> <span class="err">(</span><span class="k">$(</span><span class="nv">shell</span> <span class="nv">uname</span> -<span class="nv">s</span><span class="k">)</span><span class="err">,</span> <span class="err">Darwin)</span>
</span></span><span class="line"><span class="cl">	<span class="err">ifeq</span> <span class="err">(</span><span class="k">$(</span><span class="nv">shell</span> <span class="nv">uname</span> -<span class="nv">p</span><span class="k">)</span><span class="err">,</span> <span class="err">arm)</span>
</span></span><span class="line"><span class="cl">		<span class="c"># no difference with -march=armv8.5-a
</span></span></span><span class="line"><span class="cl"><span class="c"></span>		<span class="nv">OPTFLAGS</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl">	endif
</span></span><span class="line"><span class="cl"><span class="err">endif</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Set some additional compilter flags.
</span></span></span><span class="line"><span class="cl"><span class="c"># The most important one for us here is -Werror - treat warnings as errors.
</span></span></span><span class="line"><span class="cl"><span class="c"></span><span class="nv">PG_CFLAGS</span> <span class="o">+=</span> <span class="k">$(</span>OPTFLAGS<span class="k">)</span> -Werror -ftree-vectorize -fassociative-math -fno-signed-zeros -fno-trapping-math
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Get the pg_config binary.
</span></span></span><span class="line"><span class="cl"><span class="c"># Can be overridden by setting the PG_CONFIG environment variable.
</span></span></span><span class="line"><span class="cl"><span class="c"></span><span class="nv">PG_CONFIG</span> <span class="o">?=</span> pg_config
</span></span><span class="line"><span class="cl"><span class="c"># Include the PostgreSQL extension makefile.
</span></span></span><span class="line"><span class="cl"><span class="c"></span><span class="nv">PGXS</span> <span class="o">:=</span> <span class="k">$(</span>shell <span class="k">$(</span>PG_CONFIG<span class="k">)</span> --pgxs<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="err">include</span> <span class="k">$(</span><span class="nv">PGXS</span><span class="k">)</span>
</span></span></code></pre></div><h3 id="install-sql">Install SQL</h3>
<p>The install SQL is run on invoking <code>CREATE EXTENSION</code>. In our case, the install script is very simple, just installing the event trigger and relevant function:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- ddl_guard--1.0.sql
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">OR</span><span class="w"> </span><span class="k">REPLACE</span><span class="w"> </span><span class="k">FUNCTION</span><span class="w"> </span><span class="n">ddl_guard_check</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">-- The function returns an event trigger.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">    </span><span class="k">RETURNS</span><span class="w"> </span><span class="n">event_trigger</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">-- The function is run with the privileges of the user invoking it.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">    </span><span class="k">SECURITY</span><span class="w"> </span><span class="k">INVOKER</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="c1">-- Restrict search path to pg_catalog and pg_temp as a security measure.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">    </span><span class="k">SET</span><span class="w"> </span><span class="n">search_path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;pg_catalog, pg_temp&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">-- Specify that this is a C extension
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">    </span><span class="k">LANGUAGE</span><span class="w"> </span><span class="k">C</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">-- The module path and the function name exported from the C shared object.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">    </span><span class="k">AS</span><span class="w"> </span><span class="s1">&#39;MODULE_PATHNAME&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;ddl_guard_check&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Create the event trigger.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">EVENT</span><span class="w"> </span><span class="k">TRIGGER</span><span class="w"> </span><span class="n">ddl_guard_trigger</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">-- Trigger on the start of a DDL command.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">    </span><span class="k">ON</span><span class="w"> </span><span class="n">ddl_command_start</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">-- Execute the earlier function.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">    </span><span class="k">EXECUTE</span><span class="w"> </span><span class="k">FUNCTION</span><span class="w"> </span><span class="n">ddl_guard_check</span><span class="p">();</span><span class="w">
</span></span></span></code></pre></div><h3 id="c-extension">C Extension</h3>
<p>The C extension is where the bulk of the work happens. The full code is in the linked repo, but I&rsquo;ll break it down in chunks
here.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// Imports
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="cp">#include</span> <span class="cpf">&#34;postgres.h&#34;                   // Main Postgres headers</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&#34;fmgr.h&#34;                       // Function manager headers</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&#34;utils/fmgrtab.h&#34;              // Function manager table headers</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&#34;storage/fd.h&#34;                 // File descriptor headers</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&#34;utils/guc.h&#34;                  // GUC headers</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&#34;commands/event_trigger.h&#34;     // Event trigger headers</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&#34;miscadmin.h&#34;                  // Miscellaneous admin headers</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&#34;pgstat.h&#34;                     // Postgres statistics headers</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&#34;catalog/objectaccess.h&#34;       // Object access headers</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&#34;catalog/pg_largeobject.h&#34;     // Large object headers</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;  // Standard I/O headers</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;unistd.h&gt; // POSIX headers</span><span class="cp">
</span></span></span></code></pre></div><p>Imports are relatively straight forward - we&rsquo;re importing the Postgres headers, as well as some standard C headers for file descriptor handling and POSIX headers for file handling. Ordering is important here - some headers expect others to be included first, hence the non-alphabetical ordering.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#ifdef PG_MODULE_MAGIC
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="n">PG_MODULE_MAGIC</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="cp">#endif
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="cp">#define DDL_SENTINEL_FILE PG_STAT_TMP_DIR &#34;/ddl_guard_ddl_sentinel&#34;
</span></span></span><span class="line"><span class="cl"><span class="cp">#define LO_SENTINEL_FILE PG_STAT_TMP_DIR &#34;/ddl_guard_lo_sentinel&#34;
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span>		<span class="nf">_PG_init</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span>		<span class="nf">_PG_fini</span><span class="p">(</span><span class="kt">void</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span>		<span class="nf">write_sentinel_file</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">filename</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">Datum</span>		<span class="nf">ddl_guard_check</span><span class="p">(</span><span class="n">PG_FUNCTION_ARGS</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">void</span> <span class="nf">lob_object_access_hook</span><span class="p">(</span><span class="n">ObjectAccessType</span> <span class="n">access</span><span class="p">,</span> <span class="n">Oid</span> <span class="n">classId</span><span class="p">,</span> <span class="n">Oid</span> <span class="n">objectId</span><span class="p">,</span> <span class="kt">int</span> <span class="n">subId</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">arg</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">bool</span> <span class="n">ddl_guard_enabled</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">bool</span> <span class="n">ddl_guard_ddl_sentinel</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">bool</span> <span class="n">ddl_guard_lo_sentinel</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="n">object_access_hook_type</span> <span class="n">next_object_access_hook</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
</span></span></code></pre></div><p>Lets get our definitions out of the way, including some constants. <code>PG_MODULE_MAGIC</code> is a macro that Postgres uses to ensure the extension was built correctly, and is not, lets say, been built for a different version of Postgres. Generally, for dynamic libraries this should always be called and called once. See <a href="https://www.postgresql.org/docs/current/xfunc-c.html#XFUNC-C-DYNLOAD">Dynamic Loading</a> for more.</p>
<p>We also define some static variables to hold the state of the extension, as well as some function prototypes. <code>PG_STAT_TMP_DIR</code> is provided by Postgres, and is the temporary directory for statistics files.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* large_object_funcs contains all the create/update/destroy lobject funcs */</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">large_object_funcs</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;lo_create&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;lo_creat&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;lo_truncate&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;lo_truncate64&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;lo_unlink&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;lowrite&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;lo_from_bytea&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;be_lowrite&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;be_lo_create&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;be_lo_creat&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;be_lo_unlink&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;be_lo_truncate&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;be_lo_from_bytea&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;be_lo_truncate64&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;be_lo_import&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="s">&#34;be_lo_import_with_oid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="k">const</span> <span class="kt">int</span> <span class="n">lobject_funcs_count</span> <span class="o">=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">large_object_funcs</span><span class="p">)</span> <span class="o">/</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">large_object_funcs</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="n">Oid</span> <span class="o">*</span><span class="n">lobject_func_oids</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">int</span>	<span class="n">max_reserved_oid</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">int</span>	<span class="n">min_reserved_oid</span> <span class="o">=</span> <span class="mi">9000</span><span class="p">;</span>
</span></span></code></pre></div><p>We define an array of all the functions that are &ldquo;write&rdquo; operations for large objects, <code>large_object_funcs</code>. We also store the size of the array.</p>
<p><code>lobject_func_oids</code> is an array of OIDs for the given functions, and we set and upper and lower bound for the OIDs we&rsquo;re interested in - this will become more relevant later.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="nf">PG_FUNCTION_INFO_V1</span><span class="p">(</span><span class="n">ddl_guard_check</span><span class="p">);</span>
</span></span></code></pre></div><p><code>PG_FUNCTION_INFO_V1</code> is a macro that defines a Postgres-callable function with <a href="https://www.postgresql.org/docs/current/xfunc-c.html#XFUNC-C-V1-CALL-CONV">version-1 calling conventions</a>. This is the function that is exported and callable from Postgres.</p>
<blockquote>
<p>A brief note on C formatting: Postgres uses a particular style of formatting, BSD-like, that is enforced using the <code>pgindent</code> tool. It is <em>broadly</em> similar to K&amp;R style. I don&rsquo;t personally like it, but convention is king.</p></blockquote>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="k">const</span> <span class="n">FmgrBuiltin</span> <span class="o">*</span>
</span></span><span class="line"><span class="cl"><span class="nf">fmgr_lookupByName</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="kt">int</span>			<span class="n">i</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">fmgr_nbuiltins</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="cm">/* switched to strncmp */</span>
</span></span><span class="line"><span class="cl">		<span class="cm">/* current max len is 22 in large_object_funcs */</span>
</span></span><span class="line"><span class="cl">		<span class="k">if</span> <span class="p">(</span><span class="nf">strncmp</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">fmgr_builtins</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">funcName</span><span class="p">,</span> <span class="mi">22</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">			<span class="k">return</span> <span class="n">fmgr_builtins</span> <span class="o">+</span> <span class="n">i</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>fmgr_lookupByName</code> is a helper function that looks up a function by name in the function manager. This is used to find the OIDs of the functions we&rsquo;re interested in. It is ripped directly from Postgres&rsquo; <code>fmgr.c</code>, as it is unexported. The original function uses <code>strcmp</code>, but we switch to <code>strncmp</code> as a defensive measure.</p>
<p><code>fmgr_nbuiltins</code> is the number of built-in functions in Postgres, which are the ones we are interested in for the purposes of this extension. Exported from <code>fmgrtab.h</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">void</span>
</span></span><span class="line"><span class="cl"><span class="nf">write_sentinel_file</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">filename</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="n">FILE</span>	   <span class="o">*</span><span class="n">fp</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="n">fp</span> <span class="o">=</span> <span class="nf">AllocateFile</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="s">&#34;w&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="p">(</span><span class="n">fp</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">		<span class="nf">ereport</span><span class="p">(</span><span class="n">ERROR</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">				<span class="p">(</span><span class="nf">errcode</span><span class="p">(</span><span class="n">ERRCODE_INTERNAL_ERROR</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">				 <span class="nf">errmsg</span><span class="p">(</span><span class="s">&#34;could not create sentinel file </span><span class="se">\&#34;</span><span class="s">%s</span><span class="se">\&#34;</span><span class="s">: %m&#34;</span><span class="p">,</span> <span class="n">filename</span><span class="p">)));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="p">(</span><span class="nf">FreeFile</span><span class="p">(</span><span class="n">fp</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">		<span class="nf">ereport</span><span class="p">(</span><span class="n">ERROR</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">				<span class="p">(</span><span class="nf">errcode</span><span class="p">(</span><span class="n">ERRCODE_INTERNAL_ERROR</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">				 <span class="nf">errmsg</span><span class="p">(</span><span class="s">&#34;could not write sentinel file </span><span class="se">\&#34;</span><span class="s">%s</span><span class="se">\&#34;</span><span class="s">: %m&#34;</span><span class="p">,</span> <span class="n">filename</span><span class="p">)));</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>write_sentinel_file</code> is a helper function that writes a file to disk, with no content. It uses <code>AllocateFile</code> and <code>FreeFile</code> to hande the file creation and closing. We use <code>AllocateFile</code> over <code>fopen</code>, mostly for <a href="https://github.com/postgres/postgres/blob/REL_16_3/src/backend/storage/file/fd.c#L2512-L2528">safety reasons</a>.</p>
<p>All is pretty straightforward so far.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">bool</span>
</span></span><span class="line"><span class="cl"><span class="nf">set_lobject_func_oids</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">const</span> <span class="n">FmgrBuiltin</span> <span class="o">*</span><span class="n">entry</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">	<span class="kt">int</span>			<span class="n">i</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="n">lobject_func_oids</span> <span class="o">=</span> <span class="p">(</span><span class="n">Oid</span> <span class="o">*</span><span class="p">)</span> <span class="nf">palloc</span><span class="p">(</span><span class="n">lobject_funcs_count</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">Oid</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="p">(</span><span class="n">lobject_func_oids</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">lobject_funcs_count</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">if</span> <span class="p">((</span><span class="n">entry</span> <span class="o">=</span> <span class="nf">fmgr_lookupByName</span><span class="p">(</span><span class="n">large_object_funcs</span><span class="p">[</span><span class="n">i</span><span class="p">]))</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">		<span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="n">lobject_func_oids</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">entry</span><span class="o">-&gt;</span><span class="n">foid</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">			<span class="k">if</span> <span class="p">(</span><span class="n">entry</span><span class="o">-&gt;</span><span class="n">foid</span> <span class="o">&lt;</span> <span class="n">min_reserved_oid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">			<span class="p">{</span>
</span></span><span class="line"><span class="cl">				<span class="n">min_reserved_oid</span> <span class="o">=</span> <span class="n">entry</span><span class="o">-&gt;</span><span class="n">foid</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">			<span class="p">}</span>
</span></span><span class="line"><span class="cl">			<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">entry</span><span class="o">-&gt;</span><span class="n">foid</span> <span class="o">&gt;</span> <span class="n">max_reserved_oid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">			<span class="p">{</span>
</span></span><span class="line"><span class="cl">				<span class="n">max_reserved_oid</span> <span class="o">=</span> <span class="n">entry</span><span class="o">-&gt;</span><span class="n">foid</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">			<span class="p">}</span>
</span></span><span class="line"><span class="cl">		<span class="p">}</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>On initialisation, we need to find and store the OIDs of the large object functions. <code>set_lobject_func_oids</code> does this, by iterating over the <code>large_object_funcs</code> array and looking up the function by name. If the function is found, we store the OID and update the min and max reserved OIDs.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">void</span>
</span></span><span class="line"><span class="cl"><span class="nf">lob_object_access_hook</span><span class="p">(</span><span class="n">ObjectAccessType</span> <span class="n">access</span><span class="p">,</span> <span class="n">Oid</span> <span class="n">classId</span><span class="p">,</span> <span class="n">Oid</span> <span class="n">objectId</span><span class="p">,</span> <span class="kt">int</span> <span class="n">subId</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">arg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="kt">int</span>			<span class="n">i</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">	<span class="k">const</span> <span class="n">FmgrBuiltin</span> <span class="o">*</span><span class="n">entry</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="cm">/* check if the extension is enabled and we&#39;re not a superuser */</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="p">(</span><span class="n">ddl_guard_enabled</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nf">superuser</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">	<span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="cm">/* check if we&#39;re in sentinel mode */</span>
</span></span><span class="line"><span class="cl">		<span class="k">if</span> <span class="p">(</span><span class="n">ddl_guard_lo_sentinel</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">		<span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="k">switch</span> <span class="p">(</span><span class="n">access</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">			<span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="cm">/* if we&#39;re executing a function */</span>
</span></span><span class="line"><span class="cl">				<span class="k">case</span> <span class="nl">OAT_FUNCTION_EXECUTE</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                    <span class="cm">/* if the function is in the range of large object functions */</span>
</span></span><span class="line"><span class="cl">					<span class="k">if</span> <span class="p">(</span><span class="n">objectId</span> <span class="o">&gt;=</span> <span class="n">min_reserved_oid</span> <span class="o">&amp;&amp;</span> <span class="n">objectId</span> <span class="o">&lt;=</span> <span class="n">max_reserved_oid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">					<span class="p">{</span>
</span></span><span class="line"><span class="cl">						<span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">lobject_funcs_count</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">						<span class="p">{</span>
</span></span><span class="line"><span class="cl">                            <span class="cm">/* if the OID is in the large object OID array */</span>
</span></span><span class="line"><span class="cl">							<span class="k">if</span> <span class="p">(</span><span class="n">lobject_func_oids</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">==</span> <span class="n">objectId</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">							<span class="p">{</span>
</span></span><span class="line"><span class="cl">                                <span class="cm">/* if we successfully look up the function */</span>
</span></span><span class="line"><span class="cl">								<span class="k">if</span> <span class="p">((</span><span class="n">entry</span> <span class="o">=</span> <span class="nf">fmgr_lookupByName</span><span class="p">(</span><span class="n">large_object_funcs</span><span class="p">[</span><span class="n">i</span><span class="p">]))</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">								<span class="p">{</span>
</span></span><span class="line"><span class="cl">                                    <span class="cm">/* write a sentinel file */</span>
</span></span><span class="line"><span class="cl">									<span class="nf">write_sentinel_file</span><span class="p">(</span><span class="n">LO_SENTINEL_FILE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                                    <span class="cm">/* log a warning */</span>
</span></span><span class="line"><span class="cl">									<span class="nf">ereport</span><span class="p">(</span><span class="n">WARNING</span><span class="p">,</span> <span class="nf">errmsg</span><span class="p">(</span><span class="s">&#34;lo_guard: lobject </span><span class="se">\&#34;</span><span class="s">%s</span><span class="se">\&#34;</span><span class="s"> function call, sentinel file written&#34;</span><span class="p">,</span> <span class="n">entry</span><span class="o">-&gt;</span><span class="n">funcName</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">								<span class="p">}</span>
</span></span><span class="line"><span class="cl">							<span class="p">}</span>
</span></span><span class="line"><span class="cl">						<span class="p">}</span>
</span></span><span class="line"><span class="cl">					<span class="p">}</span>
</span></span><span class="line"><span class="cl">				<span class="k">default</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">					<span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">			<span class="p">}</span>
</span></span><span class="line"><span class="cl">		<span class="p">}</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/* call the next object access hook */</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="p">(</span><span class="n">next_object_access_hook</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">		<span class="p">(</span><span class="o">*</span><span class="n">next_object_access_hook</span><span class="p">)</span> <span class="p">(</span><span class="n">access</span><span class="p">,</span> <span class="n">classId</span><span class="p">,</span> <span class="n">objectId</span><span class="p">,</span> <span class="n">subId</span><span class="p">,</span> <span class="n">arg</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>lob_object_access_hook</code> is the meat of our detection for large object writes. It is an object access hook, which is a way to intercept and modify object access in Postgres. For more information on this type of hook, see <a href="https://github.com/taminomara/psql-hooks/blob/master/Detailed.md#object_access_hook">unofficial hook documentation</a> We check if the extension is enabled and we&rsquo;re not a superuser, and if we&rsquo;re in sentinel mode. If we are, we check if the function being executed is in the range of large object functions, and if it is, we write a sentinel file and log a warning.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">Datum</span>
</span></span><span class="line"><span class="cl"><span class="nf">ddl_guard_check</span><span class="p">(</span><span class="n">PG_FUNCTION_ARGS</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">CALLED_AS_EVENT_TRIGGER</span><span class="p">(</span><span class="n">fcinfo</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">		<span class="nf">ereport</span><span class="p">(</span><span class="n">ERROR</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">				<span class="p">(</span><span class="nf">errcode</span><span class="p">(</span><span class="n">ERRCODE_INTERNAL_ERROR</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">				 <span class="nf">errmsg</span><span class="p">(</span><span class="s">&#34;ddl_guard_check: not fired by event trigger manager&#34;</span><span class="p">)));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="p">(</span><span class="n">ddl_guard_enabled</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nf">superuser</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">	<span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="k">if</span> <span class="p">(</span><span class="n">ddl_guard_ddl_sentinel</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">		<span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="nf">write_sentinel_file</span><span class="p">(</span><span class="n">DDL_SENTINEL_FILE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">			<span class="nf">ereport</span><span class="p">(</span><span class="n">WARNING</span><span class="p">,</span> <span class="p">(</span><span class="nf">errmsg</span><span class="p">(</span><span class="s">&#34;ddl_guard: ddl detected, sentinel file written&#34;</span><span class="p">)));</span>
</span></span><span class="line"><span class="cl">			<span class="nf">PG_RETURN_VOID</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">		<span class="p">}</span>
</span></span><span class="line"><span class="cl">		<span class="nf">ereport</span><span class="p">(</span><span class="n">ERROR</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">				<span class="p">(</span><span class="nf">errcode</span><span class="p">(</span><span class="n">ERRCODE_INSUFFICIENT_PRIVILEGE</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">				 <span class="nf">errmsg</span><span class="p">(</span><span class="s">&#34;Non-superusers are not allowed to execute DDL statements&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">				 <span class="nf">errhint</span><span class="p">(</span><span class="s">&#34;ddl_guard.enabled is set.&#34;</span><span class="p">)));</span>
</span></span><span class="line"><span class="cl">	<span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nf">PG_RETURN_VOID</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>ddl_guard_check</code> is the function that is called when the event trigger is fired. It checks if the extension is enabled and we&rsquo;re not a superuser, and if we&rsquo;re in sentinel mode. If we are, we write a sentinel file and log a warning. If we&rsquo;re not in sentinel mode, we return an error and prevent the operation from being completed. We check we&rsquo;re being called from within an event trigger to prevent users from calling it directly.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">void</span>
</span></span><span class="line"><span class="cl"><span class="nf">_PG_init</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nf">DefineCustomBoolVariable</span><span class="p">(</span><span class="s">&#34;ddl_guard.enabled&#34;</span><span class="p">,</span> <span class="s">&#34;Enable or disable DDL Guard&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">							 <span class="nb">NULL</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">ddl_guard_enabled</span><span class="p">,</span> <span class="nb">false</span><span class="p">,</span> <span class="n">PGC_SUSET</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">							 <span class="nb">NULL</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nf">DefineCustomBoolVariable</span><span class="p">(</span><span class="s">&#34;ddl_guard.ddl_sentinel&#34;</span><span class="p">,</span> <span class="s">&#34;Write sentinel file for DDL statements&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">							 <span class="nb">NULL</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">ddl_guard_ddl_sentinel</span><span class="p">,</span> <span class="nb">false</span><span class="p">,</span> <span class="n">PGC_SUSET</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">							 <span class="nb">NULL</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nf">DefineCustomBoolVariable</span><span class="p">(</span><span class="s">&#34;ddl_guard.lo_sentinel&#34;</span><span class="p">,</span> <span class="s">&#34;Write sentinel file for pg_largeobject modifications&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">							 <span class="nb">NULL</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">ddl_guard_lo_sentinel</span><span class="p">,</span> <span class="nb">false</span><span class="p">,</span> <span class="n">PGC_SUSET</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">							 <span class="nb">NULL</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nf">unlink</span><span class="p">(</span><span class="n">DDL_SENTINEL_FILE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">	<span class="nf">unlink</span><span class="p">(</span><span class="n">LO_SENTINEL_FILE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="p">(</span><span class="nf">set_lobject_func_oids</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">	<span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="n">next_object_access_hook</span> <span class="o">=</span> <span class="n">object_access_hook</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">		<span class="n">object_access_hook</span> <span class="o">=</span> <span class="n">lob_object_access_hook</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">	<span class="k">else</span>
</span></span><span class="line"><span class="cl">	<span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nf">ereport</span><span class="p">(</span><span class="n">ERROR</span><span class="p">,</span> <span class="nf">errmsg</span><span class="p">(</span><span class="s">&#34;We beefed it, chief.&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nf">EmitWarningsOnPlaceholders</span><span class="p">(</span><span class="s">&#34;ddl_guard&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>_PG_init</code> is an initialisation function called immediately when a dynamic library is loaded. In it, we do a couple of things. First, we define some custom GUCs for the extension, which are used to control the behaviour of the extension. We also unlink any existing sentinel files, in case the service is restarted. Finally, we set our <code>object_access_hook</code> to our <code>lob_object_access_hook</code> function, which will intercept large object writes.</p>
<p><code>EmitWarningsOnPlaceholders</code> is mostly a prophylactic measure to ensure folks don&rsquo;t dynamically define GUCs within our namespace. It doesn&rsquo;t <em>quite</em> work like that, though - recent work on Postgres has created a new function for this purpose.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">void</span>
</span></span><span class="line"><span class="cl"><span class="nf">_PG_fini</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="p">(</span><span class="n">lobject_func_oids</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">	<span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nf">pfree</span><span class="p">(</span><span class="n">lobject_func_oids</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="n">object_access_hook</span> <span class="o">=</span> <span class="n">next_object_access_hook</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>_PG_fini</code> is a bit of an oddball - this is called if a module is unloaded. This isn&rsquo;t currently implemented in Postgres, but it is good practice to clean up after yourself. We free the memory we allocated for the large object function OIDs, and reset the <code>object_access_hook</code> to its original value.</p>
<h2 id="tests">Tests</h2>
<p>Testing is important for any extension author, and we are no exception. We use the standard Postgres regression testing approach, which executes a series of SQL files and compares the output to expected output. We have two sets of tests - one for superusers, and one for regular users. The superuser tests are the same as the regular tests, but with the superuser flag set.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- DDL tests
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">current_setting</span><span class="p">(</span><span class="s1">&#39;ddl_guard.enabled&#39;</span><span class="p">,</span><span class="w"> </span><span class="k">true</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;on&#39;</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">ddl_guard_enabled</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">current_setting</span><span class="p">(</span><span class="s1">&#39;is_superuser&#39;</span><span class="p">,</span><span class="w"> </span><span class="k">true</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;on&#39;</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">is_superuser</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">IF</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="n">foobar</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="w"> </span><span class="nb">serial</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">OR</span><span class="w"> </span><span class="k">REPLACE</span><span class="w"> </span><span class="k">FUNCTION</span><span class="w"> </span><span class="n">foobar_trigger</span><span class="p">()</span><span class="w"> </span><span class="k">RETURNS</span><span class="w"> </span><span class="k">TRIGGER</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="err">$$</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">BEGIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">RETURN</span><span class="w"> </span><span class="k">NEW</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">END</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="err">$$</span><span class="w"> </span><span class="k">LANGUAGE</span><span class="w"> </span><span class="n">plpgsql</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">DROP</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">IF</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="n">foobar</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">DROP</span><span class="w"> </span><span class="k">FUNCTION</span><span class="w"> </span><span class="k">IF</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="n">foobar_trigger</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><pre tabindex="0"><code class="language-out" data-lang="out">SELECT current_setting(&#39;ddl_guard.enabled&#39;, true) = &#39;on&#39; AS ddl_guard_enabled;
 ddl_guard_enabled
-------------------
 t
(1 row)

SELECT current_setting(&#39;is_superuser&#39;, true) = &#39;on&#39; AS is_superuser;
 is_superuser
--------------
 f
(1 row)

CREATE TABLE IF NOT EXISTS foobar (id serial PRIMARY KEY);
WARNING:  ddl_guard: ddl detected, sentinel file written
CREATE OR REPLACE FUNCTION foobar_trigger() RETURNS TRIGGER AS $$
BEGIN
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
WARNING:  ddl_guard: ddl detected, sentinel file written
DROP TABLE IF EXISTS foobar;
WARNING:  ddl_guard: ddl detected, sentinel file written
DROP FUNCTION IF EXISTS foobar_trigger;
WARNING:  ddl_guard: ddl detected, sentinel file written
</code></pre><p>There is also a set for large object tests, which are more comprehensive:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="n">bytea_output</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="k">escape</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">lo_create</span><span class="p">(</span><span class="mi">42</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="err">\</span><span class="n">lo_unlink</span><span class="w"> </span><span class="mi">42</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">lotest_stash_values</span><span class="w"> </span><span class="p">(</span><span class="n">loid</span><span class="w"> </span><span class="n">oid</span><span class="p">,</span><span class="w"> </span><span class="n">fd</span><span class="w"> </span><span class="nb">integer</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">lotest_stash_values</span><span class="w"> </span><span class="p">(</span><span class="n">loid</span><span class="p">)</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="n">lo_creat</span><span class="p">(</span><span class="mi">42</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- lobs require xacts
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">lotest_stash_values</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">fd</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">lo_open</span><span class="p">(</span><span class="n">loid</span><span class="p">,</span><span class="w"> </span><span class="k">CAST</span><span class="p">(</span><span class="n">x</span><span class="s1">&#39;20000&#39;</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">x</span><span class="s1">&#39;40000&#39;</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="nb">integer</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">lowrite</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">My hair is grey, but not with years,
</span></span></span><span class="line"><span class="cl"><span class="s1">Nor grew it white
</span></span></span><span class="line"><span class="cl"><span class="s1">    In a single night,
</span></span></span><span class="line"><span class="cl"><span class="s1">[snip]
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">lotest_stash_values</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">lo_close</span><span class="p">(</span><span class="n">fd</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">lotest_stash_values</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">END</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">lo_from_bytea</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="n">lo_get</span><span class="p">(</span><span class="n">loid</span><span class="p">))</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">newloid</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">lotest_stash_values</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="err">\</span><span class="n">gset</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">lotest_stash_values</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">fd</span><span class="o">=</span><span class="n">lo_open</span><span class="p">(</span><span class="n">loid</span><span class="p">,</span><span class="w"> </span><span class="k">CAST</span><span class="p">(</span><span class="n">x</span><span class="s1">&#39;20000&#39;</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">x</span><span class="s1">&#39;40000&#39;</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="nb">integer</span><span class="p">));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">lo_truncate</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span><span class="w"> </span><span class="mi">11</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">lotest_stash_values</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">lo_close</span><span class="p">(</span><span class="n">fd</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">lotest_stash_values</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">END</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- and more
</span></span></span></code></pre></div><p>A helper script is defined in <code>bin/test</code> to make it easier to invoke <code>make installcheck</code> with the appropriate arguments.</p>
<h2 id="considerations">Considerations</h2>
<p>Fortuantely, this extension is quite simple - a hook and an event trigger. However, there are some considerations to be made when building an extension:</p>
<ul>
<li><strong>Memory Management</strong>: Postgres has its own memory management system, and you should use it. <code>palloc</code> and <code>pfree</code> are your friends. In this extension, we don&rsquo;t use them 🙈.</li>
<li><strong>Error Handling</strong>: Postgres has its own error handling system, and you should use it. <code>ereport</code> is your friend here, and is quite customisable. The older interface for this is <code>elog</code>.</li>
<li><strong>Threading</strong>: Postgres is generally single-threaded, so you should not use threads in your extension.</li>
</ul>
<h2 id="putting-it-all-together">Putting It All Together</h2>
<p>The extension is built by running <code>make</code> in the extension directory. This will compile the extension and create a shared object. The extension can then be installed by running <code>make install</code> and <code>make installcheck</code>. It can be uninstalled with <code>make uninstall</code>.</p>
<p>For packaging, some folks opt for releasing it on <a href="https://pgxn.org/">PGXN</a>. This involves packaging the repository according to a specific set of conventions - more on this can be found <a href="https://manager.pgxn.org/howto">here</a>.</p>
<p>Alternatively, you might wish to package your extension as a Debian package or similar. Typically here the shared object is packaged and the install script installs in the correct directory. As a consequence, the package needs to be built for each Postgres version you support, and declare its dependencies in case you depend on other libraries (such as <code>json-c</code>).</p>]]></content:encoded></item><item><title>Logical Replication Guardrails</title><link>https://matt.blwt.io/post/logical-replication-guardrails/</link><pubDate>Mon, 10 Jun 2024 23:31:54 +0100</pubDate><guid>https://matt.blwt.io/post/logical-replication-guardrails/</guid><description>&lt;p&gt;I&amp;rsquo;ve been working with logical replication in PostgreSQL recently, and I wanted to share a few thoughts on how to implement some guardrails to make things easier on operators.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>I&rsquo;ve been working with logical replication in PostgreSQL recently, and I wanted to share a few thoughts on how to implement some guardrails to make things easier on operators.</p>
<h2 id="limitations">Limitations</h2>
<p>Logical replication in PostgreSQL has some <a href="https://www.postgresql.org/docs/16/logical-replication-restrictions.html">notable limitations</a>. To wit:</p>
<ul>
<li>Schema and DDL commands (e.g. <code>ALTER TABLE</code>, <code>CREATE INDEX</code>) are not replicated. Although logical replication follows the <a href="https://en.wikipedia.org/wiki/Robustness_principle">robustness principle</a> and simply error, this can lead to inconsistencies between the publisher and subscriber. This is especially important when implementing <a href="https://martinfowler.com/bliki/BlueGreenDeployment.html">blue/green</a> or <a href="https://martinfowler.com/bliki/CanaryRelease.html">canary</a> deployments of new clusters for upgrade purposes.</li>
<li>Sequence data is not replicated, requiring manual intervention to ensure sequences are updated from the publisher to the subscriber. In a control plane, this might mean storage of all the sequences across a cluster to keep track of the correct value and ensure that the subscriber is up to date prior to a cutover.</li>
<li><code>TRUNCATE</code> is replicated but has some specific edge case handling that has to be considered.</li>
<li>Large objects are not replicated.</li>
<li>Care must be taken to set up partitions appropriately on the subscriber.</li>
</ul>
<p>Lets look at approaching a solution for one of these limitations - DDL replication.</p>
<h2 id="ddl-guardrails">DDL Guardrails</h2>
<p>Generally speaking, you want to ensure that DDL commands are not run on the publisher, or that you set some kind of sentinel value or file to indicate that a failover is not possible. This particular approach is what AWS takes with their <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments-overview.html#blue-green-deployments-limitations-postgres">RDS Blue/Green</a> deployments.</p>
<p>In our case, we can look at taking it one step further with writing an extension to PostgreSQL that will prevent DDL commands from being run on the publisher. This is a simple extension that can be written in C and compiled into a shared object that can be loaded into PostgreSQL.</p>
<p>I&rsquo;ve written an example extension that does this: <a href="https://github.com/mble/ddl_guard"><code>ddl_guard</code></a>.</p>
<p>Currently, it implements a GUC (<code>ddl_guard.enabled</code>) that can be set to <code>on</code> or <code>off</code>. When set to <code>on</code>, it will prevent DDL commands from being run on the publisher for non-superusers.</p>
<p>The actual implementation is that of an event trigger and corresponding event trigger function that prevents DDL command execution:</p>
<pre tabindex="0"><code>CREATE TABLE IF NOT EXISTS foobar (id serial PRIMARY KEY);
ERROR:  Non-superusers are not allowed to execute DDL statements
HINT:  ddl_guard.enabled is set.
</code></pre><p>Additionally, the GUC can only be modified by superusers:</p>
<pre tabindex="0"><code>SET ddl_guard.enabled TO off;
ERROR:  permission denied to set parameter &#34;ddl_guard.enabled&#34;
</code></pre><p>An alternative option would be to write a sentinel file to disk on detection of a DDL command, and have a control plane check for the existence of the file. The extension implements this mode:</p>
<pre tabindex="0"><code>SET ddl_guard.ddl_sentinel TO on;
CREATE TABLE IF NOT EXISTS foobar (id serial PRIMARY KEY);
WARNING:  ddl_guard: ddl detected, sentinel file written
CREATE TABLE
</code></pre><p>The control plane can then check for the existence of the sentinel file and prevent a failover from occurring.</p>
<h2 id="future-work">Future Work</h2>
<p>In the future, such an extension should be expanded to detect when the large object facility is used and write a similar sentinel file, or otherwise block operations that alter large objects.</p>]]></content:encoded></item><item><title>A Love Letter to Giant Robots</title><link>https://matt.blwt.io/post/a-love-letter-to-giant-robots/</link><pubDate>Sat, 01 Jun 2024 15:32:47 +0100</pubDate><guid>https://matt.blwt.io/post/a-love-letter-to-giant-robots/</guid><description>&lt;p&gt;Ah, &lt;em&gt;giant robots&lt;/em&gt;. I grew up thinking they are the coolest things ever, and I still do. My first exposure to them was probably through 1994&amp;rsquo;s &lt;em&gt;BattleTech&lt;/em&gt; animated series, where Adam Steiner piloted an AXM-2N &lt;em&gt;Axman&lt;/em&gt;, in its frankly ridiculous neon green and purple paint job. And from there, &lt;em&gt;well&lt;/em&gt;. I was hooked. Lets explore a little about some history, some of my favourite giant robot media, and even a &lt;em&gt;small&lt;/em&gt; work-related thing.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>Ah, <em>giant robots</em>. I grew up thinking they are the coolest things ever, and I still do. My first exposure to them was probably through 1994&rsquo;s <em>BattleTech</em> animated series, where Adam Steiner piloted an AXM-2N <em>Axman</em>, in its frankly ridiculous neon green and purple paint job. And from there, <em>well</em>. I was hooked. Lets explore a little about some history, some of my favourite giant robot media, and even a <em>small</em> work-related thing.</p>
<h2 id="a-note-on-terminology">A Note on Terminology</h2>
<p>Giant robot, or <em>mecha</em>, media is a broad church. That said, there are generally two accepted terms for the two main types of giant robots: the <em>Super Robot</em> and the <em>Real Robot</em>. The aforementioned AXM-2N <em>Axman</em> is thoroughly a Real Robot - they are mass produced (kind of) and require visible maintenance and care of the systems involved, even if some handwavy science is involved. Perhaps the clearest definition is that in Real Robot media, the robots are <em>tools</em> - they are used by the characters, but they are not the characters themselves.</p>
<p>On the other, hand, we have the Super Robots. These <em>are</em> characters in their own right, and are typically unique. Expect explanations of their powers to be virtually non-existent, and for them to be powered by concepts rather than fuel cells or nuclear reactors. <em>Transformers</em> is probably the most famous Western example of a Super Robot - sentient machines powered by Sparks (in the appropriate continuity).</p>
<p>With that out of the way, lets talk about some of my favourite giant robot media. I&rsquo;ll be covering a mix of Super and Real Robot, across TV, film, video games and a little bit of table-top gaming.</p>
<h2 id="contents">Contents</h2>
<ol>
<li><a href="#tv--film">TV &amp; Film</a></li>
<li><a href="#video-games">Video Games</a></li>
<li><a href="#tabletop-rpgs">Tabletop RPGs</a></li>
<li><a href="#bonus-a-small-work-related-thing">Bonus: A Small Work-Related Thing</a></li>
<li><a href="#closing-words">Closing Words</a></li>
</ol>
<h2 id="tv--film">TV &amp; Film</h2>
<h3 id="mobile-suit-gundam-the-08th-ms-team">Mobile Suit Gundam: The 08th MS Team</h3>
<figure><img src="/giant-robots/08th-ms-team.webp"
    alt="A promotional poster for &#34;Mobile Suit Gundam: The 08th MS Team&#34;, an anime series. It features a large, heavily weathered and battle-damaged Gundam mech (robot) standing in a dense jungle setting. The Gundam is primarily white with red and blue accents and holds a large shield with the number &#34;08&#34; visible on it. The shield and the Gundam both show signs of rust and damage. Above the Gundam, three soldiers are descending from the sky using parachutes. The background consists of lush green foliage, tall trees, and a blue sky with some clouds. The title &#34;Mobile Suit Gundam: The 08th MS Team&#34; is prominently displayed at the bottom in bold black and red letters. The logo for Sunrise, the studio behind the series, is located in the lower right corner of the image."><figcaption>
      <h4>Mobile Suit Gundam: The 08th MS Team Poster. Copyright Sunrise / Bandai Namco Filmworks, Inc. 1996</h4>
    </figcaption>
</figure>

<p>I am a <em>huge</em> fan of the <em>Mobile Suit Gundam</em> franchise, and <em>The 08th MS Team</em> OVA (Original Video Animation) is probably my favourite entry. It is a Real Robot series, set during the One Year War of the Universal Century timeline. The series follows the titular 08th MS Team, a group of Federation soldiers stationed in Southeast Asia. The series is notable for its focus on the human cost of war, and the fact that the main character, Shiro Amada, is a <em>lieutenant</em> - a rarity in anime. I have kits of both the RX-79[G] <em>Gundam Ground Type</em> and the MS-07B-3 <em>Gouf Custom</em> on my shelf, and they are some of my favourites.</p>
<p>What makes this series stand out in the wider <em>Gundam</em> franchise is its focus on the <em>grunts</em> - there are no heroes here, just soldiers trying to survive a war. There is a heavy emphasis on the horrors of war and &ldquo;life in the trenches&rdquo; - resources are scarce, and the soliders are often forced to scavenge parts from the battlefield to keep their machines running. The series also features a <em>very</em> good romance subplot - not every show can manage to pull off a slow-burn romance over 12 episodes, but its managed here.</p>
<p>At the time of writing, <em>The 08th MS Team</em> is available to stream on Hulu and Disney+. Obviously, go and watch the rest of the <em>Gundam</em> UC timeline too, but with special recommendations for working up to <em>Gundam Unicorn</em> - plenty of watchlists out there to help you with that - <a href="https://www.denofgeek.com/tv/how-to-watch-gundam-anime-franchise-order/">this one</a> is quite good, though I&rsquo;d put <em>THE ORIGIN</em> later on, after <em>Unicorn</em>.</p>
<p><sup>Also Karen best girl.</sup></p>
<h3 id="patlabor-the-movie">Patlabor: The Movie</h3>
<figure><img src="/giant-robots/patlabor-the-movie.webp"
    alt="The image is a promotional poster for &#34;Patlabor: The Movie&#34;, an anime film. It features a character standing in a side profile with their head tilted back, looking up. The character is a young woman with short, reddish-brown hair. She is wearing a uniform consisting of a white shirt with short sleeves, a black tie, and an orange vest. The vest has a patch on the sleeve, and she also has blue trousers and brown shoes with white spats. The character holds a white helmet under her arm, indicating that she is likely part of a police unit. The background is plain white, with no additional scenery or context. The title &#34;PATLABOR&#34; is prominently displayed in large, bold, black letters across the bottom of the image. Above the English title is the Japanese title written in smaller, black characters."><figcaption>
      <h4>Patlabor: The Movie Poster. Copyright Headgear / Bandai Visual Co., Ltd. 1989</h4>
    </figcaption>
</figure>

<blockquote>
<p>In the Criminal Justice System, Humongous Mecha-based offenses are considered especially heinous. In Tokyo, the dedicated officers who deal with these vicious felonies are an elite squad known as the Special Vehicles Unit. These are their stories.</p>
<p><span style="font-variant-caps: small-caps;">dun-dun</span></p></blockquote>
<p><sup><a href="https://tvtropes.org/pmwiki/pmwiki.php/Franchise/Patlabor">From TVTropes</a></sup></p>
<p>When writing this, I was trying to decide if I liked <em>Patlabor: The Movie</em>, the OVA, or <em>Patlabor: The Movie 2</em> more. I recommend all of them, especially <em>The Movie 2</em> if you end up watching this one.</p>
<p><em>Patlabor</em> is a Real Robot series (perhaps one of the <em>realest</em> along with <em>Armored Trooper VOTOMS</em>), set in a near-future Tokyo where giant robots - Labors - are used for construction and law enforcement. The series follows the members of the Special Vehicles Section 2, a division of the Tokyo Metropolitan Police Department that deals with crimes involving Labors.</p>
<p>So why <em>The Movie</em>? It&rsquo;s a great standalone story, following a series of mysterious Labor malfunctions that cause significant destruction and endanger lives. These incidents are traced back to a new advanced operating system called the HOS, developed by a programmer named Eiichi Hoba. As the SV2 team investigates, they discover that Hoba has committed suicide, leaving behind a cryptic message and a legacy of chaos through his software. If you are a software engineer, you will understand that this is a <em>big mood</em>.</p>
<p>What I like about <em>The Movie</em> is that, again, its a slow burn where the Labors, although cool as hell, are falliable tools. The <em>Patlabor</em> series always feels like we are only one or two technological steps away from it being made real, and <em>The Movie</em> highlights the complex interplay between technology and the people that create and operate it.</p>
<p>At the time of writing, <em>Patlabor: The Movie</em> is not available for streaming. All three movies (though I&rsquo;m not a huge fan of <em>WXIII</em>) are available on Blu-Ray.</p>
<h3 id="space-runaway-ideon">Space Runaway Ideon</h3>
<figure><img src="/giant-robots/ideon.webp"
    alt="A promotional poster for &#34;Space Runaway Ideon&#34;, an anime series and movie collection. The central figure in the image is a large red robot, known as the Ideon, depicted from the waist up. The Ideon has a humanoid shape with a square, mechanical face and prominent V-shaped antennas on its head. Its chest features various rectangular panels and a circular emblem. The background shows a view of space with a planet&#39;s horizon visible at the bottom. Above the planet, there is a bright light source, possibly a star, casting a glow. The title &#34;IDEON&#34; is written in large, white letters across the top of the image, with the words &#34;SPACE RUNAWAY&#34; in smaller white letters inside the &#34;O&#34; in &#34;IDEON.&#34; Below the title, it states &#34;COMPLETE SERIES &#43; MOVIES&#34; in smaller white text."><figcaption>
      <h4>Space Runaway Ideon Poster. Copyright Sunrise / Bandai Namco Filmworks, Inc. 1980</h4>
    </figcaption>
</figure>

<p>Oh, <em>Space Runaway Ideon</em>. What happens when the father of modern Real Robot stories, Yoshiyuki Tomino, directs a Super Robot show immediately after <em>Mobile Suit Gundam</em>? You get <em>Ideon</em>.</p>
<p>In brief, <em>Ideon</em> follows the crew of the &ldquo;Solo Ship&rdquo; as they flee from the pursuit of the Buff Clan with their mysterious giant robot, the Ideon. <em>Ideon</em> subverts many of the tropes of the Super Robot genre - the Ideon is not a hero, but a weapon of mass destruction that is poorly understood, by our heroes and the Buff Clan. Rather than taking a &ldquo;monster of the week&rdquo; approach like many of its predecessors, <em>Ideon</em> works slowly over the course of 39 episodes and two movies, exploring existential dread, cosmic horror, and the human condition.</p>
<p>Yeah, no wonder Hideaki Anno cites <em>Ideon</em> as a major influence on <em>Neon Genesis Evangelion</em>.</p>
<p>I love <em>Ideon</em> for its sheer ambition - it is a show that is not afraid to ask big questions, and it is a show that is not afraid to answer them. The show is not perfect - the animation quality is inconsistent, and the pacing can be a little slow at times. But the show&rsquo;s ambition and willingness to take risks make it a standout in the Super Robot genre. There hasn&rsquo;t really been anything quite like it since, except maybe <em>Bokurano</em> and the aforementioned <em>Evangelion</em>.</p>
<p>At the time of writing, <em>Space Runaway Ideon</em> is not avaialble for streaming. The series and movies are out of print - you may be able to find them on eBay or similar in Blu-Ray.</p>
<h3 id="dai-guard">Dai-Guard</h3>
<figure><img src="/giant-robots/dai-guard.webp"
    alt="A promotional poster for the anime series &#34;Dai-Guard.&#34; It features a large group of characters posing in front of the giant robot, Dai-Guard. The robot is prominently displayed in the background, towering over the characters with its distinctive red, black, and yellow design. The Dai-Guard has a stern, humanoid face and a bright, star-shaped crest on its forehead. The foreground features the main characters standing confidently in front of Dai-Guard. At the bottom of the poster, the title &#34;Dai-Guard&#34; is prominently displayed in bold, blue and red letters, with the subtitle &#34;Terrestrial Defense Corp.&#34; written above it in smaller, yellow letters."><figcaption>
      <h4>Dai-Guard Poster. Copyright Studio Xebec 1999.</h4>
    </figcaption>
</figure>

<blockquote>
<p><a href="https://www.youtube.com/watch?v=NQEc9hjC6ys">Listen to the opening while reading this</a>.</p></blockquote>
<p>So we&rsquo;ve had two by the books Real Robot series, a highly subversive Super Robot series, and now we have <em>Dai-Guard</em>. <em>Dai-Guard</em> is&hellip; hard to define. Ostensibly, this is Real Robot show, but the emponymous Dai-Guard <em>really, <strong>really</strong></em> wants to be a Super Robot - again, listen to that opening!</p>
<p>If you have ever worked for any large corporations, the trappings of the 21st Century Security Corporation will be <em>painfully</em> familiar. Sure, you own a giant robot, but its been mothballed for years. Now you&rsquo;ve got to pay for upkeep, deal with the government, fill out forms, and <em>oh god</em> the paperwork. <em>Dai-Guard</em> is a show about the <em>bureaucracy</em> of giant robots, and it is <em>hilarious</em>. There is a fantastic line where, doing a lot of &ldquo;hurry up and wait&rdquo;, one of the pilots says something along the lines of &ldquo;I&rsquo;ve watched a lot of giant robot anime all my life, and they never mentioned this part&rdquo;.</p>
<p>Beyond the relatively silly trappings, there is - you might be seeing a theme here - a lot of focus on the people, rather than the robot. The alien threat of the Heterodyne stops being a threat about a third of the way through the series - the real threat is the bureaucracy, balancing the budget and dealing with megalomanical bosses - something anything a corporate worker can relate to.</p>
<p>As mentioned earlier, its a Real Robot show, but it <em>wants</em> to be a Super Robot show. The Dai-Guard is a unique machine, and the pilots are unique people, but Dai-Guard isn&rsquo;t a very good robot and the pilots aren&rsquo;t very good either. No matter how much Akagi Shunsuke wants to win through the power of hot-bloodedess, the square-cube law is a harsh mistress and no laws of physics will be violated today.</p>
<p>At the time of writing, <em>Dai-Guard</em> is not avaialble for streaming. It is also out of print, so you may be able to find it on eBay or similar in DVD.</p>
<h3 id="gunbuster">GunBuster</h3>
<figure><img src="/giant-robots/gunbuster.webp"
    alt="A promotional poster for the anime series &#34;GunBuster.&#34; The central figure in the image is a large robot, known as GunBuster, standing tall against a vibrant, dramatic sky. The robot has a predominantly black and orange color scheme with a humanoid shape. It has large, wing-like appendages on its back and a distinctive headpiece with a star-shaped crest. The robot&#39;s arms are folded across its chest, giving it a powerful and imposing presence. In the foreground, three female characters are standing confidently, each in a different pose. The title &#34;GUNBUSTER&#34; is prominently displayed in large, bold, red letters across the top of the image."><figcaption>
      <h4>GunBuster Poster. Copyright Gainax Co., Ltd. 1998</h4>
    </figcaption>
</figure>

<p><span style="font-variant-caps: small-caps">inazuma kick!</span></p>
<p>So, <em>GunBuster</em>. Before <em>Evangelion</em> and <em>Gurren Lagann</em>, there was <em>GunBuster</em>, and it is amazing. It isn&rsquo;t <em>quite</em> a deconstruction, or even a parody, but crammed full of love for Super Robots and classic tennis anime. Yes, tennis anime. The full title - <em>Aim for the Top! GunBuster</em> - is a reference to <em>Aim for the Ace!</em>, a classic tennis anime, and the show is full of references to classic Super Robot shows like <em>Getter Robo</em> and <em>Mazinger Z</em>.</p>
<p>The story of <em>GunBuster</em> is very much a classic coming-of-age story - Noriko Takaya evolves from an insecure trainee to a confident and capable pilot, while showing a lot of emotional depth. What is also particularly interesting is how time dilation is used to great effect, and the very real impacts it has on the characters - see <em>Interstellar</em> for a more recent example of this.</p>
<p>We can&rsquo;t talk about <em>GunBuster</em> without its sequel, <em>Diebuster</em>. <em>Diebuster</em> is a very different beast - it is a lot more abstract and experimental, and is a lot more of a deconstruction of the Super Robot genre. It is also <em>very</em> good, and I recommend watching both - I don&rsquo;t think you can do one without the other.</p>
<p>Both have absolutely <em>banging</em> soundtracks. <a href="https://www.youtube.com/watch?v=afXl2o82mmA">Groovin&rsquo; Magic</a> is a <em>bop</em>, and <a href="https://www.youtube.com/watch?v=ZLhPBQpXafY">Active Heart</a> just <em>oozes</em> 80s cheese, in the best way.</p>
<p>At the time of writing, <em>GunBuster</em> and <em>DieBuster</em> aren&rsquo;t available for streaming, but they are available on Blu-Ray.</p>
<h3 id="what-about-evangelion86code-geassgetter-robowhatever">What about Evangelion/86/Code Geass/Getter Robo/whatever?</h3>
<p>Look, I could write a whole post on <em>Gundam</em> alone, let alone getting into <em>Evangelion</em> and other more well known stuff. These ones are my particular favourites. You could work your way through the <a href="https://anilist.co/search/anime?genres=Mecha&amp;sort=SCORE_DESC">Anilist top list for <em>mecha</em></a> or the <a href="https://myanimelist.net/anime/genre/18/Mecha">MyAnimeList top list for <em>mecha</em></a> if you wanted to, but this was meant to be a more curated, personal list.</p>
<h3 id="some-other-recommendations">Some other recommendations</h3>
<ul>
<li><em>Armored Trooper VOTOMS</em> - a very Real Robot show, with a focus on the horrors of war.</li>
<li><em>Armor Hunter Mellowlink</em> - a spin-off of <em>VOTOMS</em>, with a &ldquo;man vs. machine&rdquo; focus and very tight storytelling.</li>
<li><em>King of Braves GaoGaiGar</em> - a Super Robot show that is <em>very</em> Super Robot, with an iconic opening.</li>
<li><em>Shin Getter Robo</em> - Really a toss up between this an <em>Getter Robo: Armageddon</em>. There is not much of plot - just a lot of robot combat action.</li>
</ul>
<figure><img src="/giant-robots/ryoma.webp"
    alt="A front on portrait of Ryoma Nagare from Getter Robo. He is a man with a serious expression, long black hair with the famous Go Nagai sideburns of hot bloodedness, a grey t-shirt and beige overcoat. A long, fraying red scarf is dramatically wrapped around his neck."><figcaption>
      <h4>Ryoma Nagare, king of hot blooded sideburns. Copyright Brain&#39;s Base 2004.</h4>
    </figcaption>
</figure>

<h2 id="video-games">Video Games</h2>
<p>Frankly, there is a precious dearth of good <em>mecha</em> games, for some reason. They are mostly Real Robot focused - outside of <em>Super Robot Wars</em> there is precious little Super Robot representation. Here are three of my personal favourites:</p>
<h3 id="wolfstride">Wolfstride</h3>
<figure><img src="/giant-robots/wolfstride.webp"><figcaption>
      <h4>Wolfstride. Copyright OTA IMON Studios 2021.</h4>
    </figcaption>
</figure>

<p><em>Wolfstride</em> is a bit of an odd duck. Three ex-cons, a junkyard mecha, and a dream. Entirely in black and white, a hybrid pixel/anime art style and an excellent soundtrack make for a compelling experience. It&rsquo;s a mix of turn-based combat and visual novel, and really is quite goood.</p>
<p>Available on <a href="https://store.steampowered.com/app/1331210/Wolfstride/">Steam</a>.</p>
<h3 id="into-the-breach">Into the Breach</h3>
<figure><img src="/giant-robots/into-the-breach.webp"><figcaption>
      <h4>Into the Breach. Copyright Subset Games 2018.</h4>
    </figcaption>
</figure>

<p><em>Into the Breach</em> is a turn-based strategy game from the makers of <em>FTL: Faster Than Light</em>. It is a game about the last stand of humanity against an alien threat, and you control a squad of mechs to protect the cities of Earth.</p>
<p>Fundamentally, its a puzzle game. The robots feel chunky, and have some really solid designs to them, even in minature. Another pixel art game.</p>
<p>Available on <a href="https://store.steampowered.com/app/590380/Into_the_Breach/">Steam</a>.</p>
<h3 id="chained-echoes">Chained Echoes</h3>
<figure><img src="/giant-robots/chained-echoes.webp"><figcaption>
      <h4>Chained Echoes. Copyright Ark Heiral 2022.</h4>
    </figcaption>
</figure>

<p>You&rsquo;re probably thinking &ldquo;this isn&rsquo;t a <em>mecha</em> game&rdquo;, and you&rsquo;d be wrong. Sure, it has all the traditional trappings of a classic fantasy JRPG, but the inclusion of Mechs means it totally counts. Huge amounts of content, epic story and a phenomenal battle system.</p>
<p>Available on <a href="https://store.steampowered.com/app/1229240/Chained_Echoes/">Steam</a>.</p>
<h3 id="visual-novels">Visual Novels</h3>
<p>OK, I lied about the three games. Visual novels are a different beast and aren&rsquo;t for everyone. That said, there are two I would recommend: <em>Muv-Luv</em> (and <em>Muv-Luv Alternative</em>) and <em>Full Metal Daemon Muramasa</em>.</p>
<p>I won&rsquo;t spend too much time on these, due to their niche appeal, but thought I&rsquo;d throw them in anyway.</p>
<p>Special shout out to <em>Demonbane</em> - I can&rsquo;t recommend it directly because of the nature of its content, but it gets credit for perhaps having the largest mecha in <em>any</em> medium (at least, in a prequel novel).</p>
<h3 id="what-about-armored-corezone-of-the-endersfront-missionwhatever">What about Armored Core/Zone of the Enders/Front Mission/whatever?</h3>
<p>You&rsquo;ve probably heard about them, and they are all excellent. I haven&rsquo;t yet played <em>Armored Core VI</em>, but I&rsquo;ve heard excellent things. I adore <em>Zone of the Enders</em>, but I haven&rsquo;t played it for some time. <em>Front Mission</em> is something I&rsquo;m not super familiar with, I&rsquo;m embareassed to say.</p>
<h2 id="tabletop-rpgs">Tabletop RPGs</h2>
<p>Last but not least, a couple of tabletop RPGs that I really ennjoy.</p>
<h3 id="lancer">Lancer</h3>
<figure><img src="/giant-robots/lancer.webp"><figcaption>
      <h4>Lancer. Copyright Massif Press 2019.</h4>
    </figcaption>
</figure>

<p><a href="https://massifpress.com/lancer">Lancer</a> is everything I have ever wanted from a giant robot tabletop RPG. With art by Abaddon of <em>Kill Six Billion Demons</em> fame, and a system that is <em>tight</em>, <em>Lancer</em> is a game about mech pilots, their mechs, and the world they inhabit. The game is set in a far future where humanity has spread to the stars, and the players are members of a mercenary company that pilots mechs called <em>frames</em>.</p>
<p>There are a bunch of narratives and settings available, both from the community and from Massif Press themselves. COMP/CON is a fantastic tool for building characters, keeping track of things and running the game. And you&rsquo;ll need it - the game is very crunchy and requires a lot of bookkeeping. There is so, so much lore, but that is also half the fun.</p>
<p>It is, however, very mission based, and that might not jive well with a group that enjoys more of the intermission time. It requires a lot of effort from the GM to make a compelling campaign.</p>
<h3 id="salvage-union">Salvage Union</h3>
<figure><img src="/giant-robots/salvage-union.webp"><figcaption>
      <h4>Salvage Union. Copyright Leyline Press 2023.</h4>
    </figcaption>
</figure>

<p><a href="https://leyline.press/products/salvage-union-core-book">Salvage Union</a>, on the flip side, is much less crunchy and takes the mechanics from <a href="https://www.adventure.game/">Quest</a> - a single d20 is all this is needed.</p>
<p>The game is set in a post-apocalyptic world where the players are members of a salvage crew, tasked with recovering lost technology from the ruins of the old world. The game is very much about exploration and discovery, and the players are encouraged to build the world as they play. There is a lot of focus on narrative, and half the fun is building and maintaining your mech and Crawler as you go about, doing jobs that might or might not fit into a larger narrative.</p>
<p>This game is basically the anthesis of <em>Lancer</em> - it is very rules light, and the focus is on the narrative rather than the mechanics. It is a lot easier to run, and a lot more forgiving to new players.</p>
<p>Also dieselpunk is just straight cool, and so are the Meld.</p>
<h3 id="what-about-battletechheavy-gearwhatever">What about Battletech/Heavy Gear/whatever?</h3>
<p>I haven&rsquo;t played <em>Battletech</em> in years, and I&rsquo;ve never played <em>Heavy Gear</em>. <em>MechWarrior: Destiny</em> looks like it would be up my alley in terms of being a narrative-focused game, but I haven&rsquo;t had a chance to play it yet.</p>
<h2 id="bonus-a-small-work-related-thing">Bonus: A Small Work-Related Thing</h2>
<figure><img src="/giant-robots/heroku-mecha.webp"><figcaption>
      <h4>Many, many years ago. Copyright Heroku, Inc 2011.</h4>
    </figcaption>
</figure>

<p>Looking into the distant past, before the era of <code>standard-0</code> Heroku Postgres plans, the plan naming was quite different and a lot more whimsical. The <code>mecha</code> plan was the largest, and the most powerful, and used the iconic visage of the RX-78-2 <em>Gundam</em> as its logo.</p>
<p>For a long time, <code>mecha</code> was the highest tier, until a shake up of the plans where in the Enterprise tier, <code>ryu</code> was introduced. <code>crane</code>, <code>kappa</code>, <code>ronin</code> and <code>fugu</code> also went away, to be replaced by <code>yanari</code> and <code>tengu</code>.</p>
<h2 id="closing-words">Closing Words</h2>
<p>Heck yeah, giant robots. I could have whole posts on <em>Gundam</em> alone (and I haven&rsquo;t even touched gunpla!). Always remember:</p>
<blockquote>
<p><a href="https://www.youtube.com/watch?v=5v3AV95jMqM"><em>DO THE IMPOSSIBLE</em><br>
<em>SEE THE INVISIBLE</em><br>
<em>ROW ROW FIGHT THE POWER!</em></a></p></blockquote>
<figure><img src="/giant-robots/kamina.webp"><figcaption>
      <h4>Copyright Studio Gainax 2007.</h4>
    </figcaption>
</figure>]]></content:encoded></item><item><title>Shunn</title><link>https://matt.blwt.io/post/shunn/</link><pubDate>Fri, 31 May 2024 18:33:19 +0100</pubDate><guid>https://matt.blwt.io/post/shunn/</guid><description>&lt;p&gt;The Shunn manuscript format is a set of guidelines for writers to follow when submitting manuscripts to publishers. It was created by author William Shunn and is widely used in the publishing industry. Here is a brief demonstration of a tool I wrote to facilitate generating manuscripts in this format.&lt;/p&gt;</description><content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>The Shunn manuscript format is a set of guidelines for writers to follow when submitting manuscripts to publishers. It was created by author William Shunn and is widely used in the publishing industry. Here is a brief demonstration of a tool I wrote to facilitate generating manuscripts in this format.</p>
<p>The Shunn format is designed to be easy to read and navigate, with clear headings and subheadings to help guide the reader through the manuscript. It also includes guidelines for formatting dialogue, block quotes, and other elements of the text. By following these guidelines, writers can ensure that their manuscripts are well-organized and easy to follow, which can help them stand out in a competitive publishing market.</p>
<p>I wrote a small tool to help writers format their manuscripts according to the Shunn guidelines. You can find it <a href="https://github.com/mble/shunn">here</a>. The tool takes in Markdown, some metadata, and outputs a PDF formatted to Shunn guidelines. It also includes automatic word counting, thanks to a Lua script - my favourite <a href="/post/lua-the-little-language-that-could/">little language</a>.</p>
<p>Here is a small screenshot of the input and output, generated through <code>bin/prepare test/test_input.md</code>:</p>
<figure><img src="/shunn-example.webp"
    alt="A screenshot showing Visual Studio Code on the left, with a Markdown extract of E. M. Forster&#39;s A Room With A View, and a PDF version of that extract on the right, generated by the Shunn tool."><figcaption>
      <h4>Markdown on the left, Shunn-formatted PDF on the right</h4>
    </figcaption>
</figure>]]></content:encoded></item></channel></rss>