1<!DOCTYPE html><html lang="en-AU"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Marty</title><meta name="description" content="A personal site by Marty â software, hardware, AI, gaming, finance, homesteading, and whatever else is on my mind."><meta name="author" content="Marty"><link rel="canonical" href="https://martyw.dev/"><meta property="og:type" content="website"><meta property="og:site_name" content="martyw.dev"><meta property="og:locale" content="en_AU"><meta property="og:title" content="Marty"><meta property="og:description" content="A personal site by Marty â software, hardware, AI, gaming, finance, homesteading, and whatever else is on my mind."><meta property="og:url" content="https://martyw.dev/"><meta property="og:image" content="https://martyw.dev/og-default.png"><meta property="og:image:type" content="image/png"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630"><meta property="og:image:alt" content="martyw.dev â Things I make, learn, and think about."><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="Marty"><meta name="twitter:description" content="A personal site by Marty â software, hardware, AI, gaming, finance, homesteading, and whatever else is on my mind."><meta name="twitter:image" content="https://martyw.dev/og-default.png"><meta name="twitter:image:alt" content="martyw.dev â Things I make, learn, and think about."><link rel="icon" href="/favicon.svg" type="image/svg+xml"><link rel="alternate" type="application/rss+xml" title="Marty â RSS" href="/rss.xml"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&display=swap" media="print" onload="this.media='all'">
1<script> 2 (function () { 3 try { 4 const stored = localStorage.getItem("theme"); 5 const prefersDark = window.matchMedia( 6 "(prefers-color-scheme: dark)", 7 ).matches; 8 const mode = stored ?? (prefersDark ? "dark" : "light"); 9 if (mode === "dark") document.documentElement.classList.add("dark"); 10 } catch (_) {} 11 })(); 12 </script>
12<link rel="stylesheet" href="/_astro/_slug_.9N3vsjBc.css"> 13<style>.tag-list[data-astro-cid-lga65v7i][data-collapsible] .tag-list__hidden[data-astro-cid-lga65v7i]{display:none}.tag-list[data-astro-cid-lga65v7i][data-collapsible]:hover .tag-list__hidden[data-astro-cid-lga65v7i],.tag-list[data-astro-cid-lga65v7i][data-collapsible][data-expanded] .tag-list__hidden[data-astro-cid-lga65v7i],.tag-list[data-astro-cid-lga65v7i][data-collapsible]:focus-within .tag-list__hidden[data-astro-cid-lga65v7i]{display:list-item}.tag-list[data-astro-cid-lga65v7i][data-collapsible]:hover .tag-list__more[data-astro-cid-lga65v7i],.tag-list[data-astro-cid-lga65v7i][data-collapsible][data-expanded] .tag-list__more[data-astro-cid-lga65v7i]{display:none} 14</style></head> <body class="flex flex-col min-h-dvh"> <header class="border-b border-[color:var(--border)] bg-[color:var(--bg)]/80 backdrop-blur sticky top-0 z-20"> <div class="container-wide flex items-center justify-between gap-4 py-4"> <a href="/" class="font-semibold text-lg tracking-tight no-underline text-[color:var(--ink)] hover:text-[color:var(--accent)]"> 15martyw<span class="text-[color:var(--accent)]">.</span>dev 16</a> <nav class="hidden sm:flex items-center gap-1 text-sm"> <a href="/" class="px-3 py-1.5 rounded-md no-underline text-[color:var(--accent-strong)] bg-[color:var(--accent-soft)]"> Home </a><a href="/posts/" class="px-3 py-1.5 rounded-md no-underline text-[color:var(--ink-soft)] hover:text-[color:var(--ink)] hover:bg-[color:var(--bg-soft)]"> Posts </a><a href="/notes/" class="px-3 py-1.5 rounded-md no-underline text-[color:var(--ink-soft)] hover:text-[color:var(--ink)] hover:bg-[color:var(--bg-soft)]"> Notes </a><a href="/about/" class="px-3 py-1.5 rounded-md no-underline text-[color:var(--ink-soft)] hover:text-[color:var(--ink)] hover:bg-[color:var(--bg-soft)]"> About </a> </nav> <div class="flex items-center gap-2"> <button type="button" data-search-open class="inline-flex items-center justify-center rounded-md w-9 h-9 border border-[color:var(--border)] bg-[color:var(--surface)] hover:bg-[color:var(--bg-soft)] transition-colors" aria-label="Search (press /)" title="Search â press /"> <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <circle cx="11" cy="11" r="7"></circle> <path d="m20 20-3.5-3.5"></path> </svg> </button> <button id="theme-toggle" type="button" aria-label="Toggle colour theme" class="inline-flex items-center justify-center rounded-md w-9 h-9 border border-[color:var(--border)] bg-[color:var(--surface)] hover:bg-[color:var(--bg-soft)] transition-colors"> <svg class="theme-icon-light w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <circle cx="12" cy="12" r="4"></circle> <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"></path> </svg> <svg class="theme-icon-dark w-4 h-4 hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> </svg> </button>
16<script> 17 (() => { 18 const root = document.documentElement; 19 const btn = document.getElementById("theme-toggle"); 20 if (!btn) return; 21 22 const apply = (mode) => { 23 if (mode === "dark") { 24 root.classList.add("dark"); 25 document.querySelector(".theme-icon-light")?.classList.add("hidden"); 26 document.querySelector(".theme-icon-dark")?.classList.remove("hidden"); 27 } else { 28 root.classList.remove("dark"); 29 document.querySelector(".theme-icon-light")?.classList.remove("hidden"); 30 document.querySelector(".theme-icon-dark")?.classList.add("hidden"); 31 } 32 }; 33 34 apply(root.classList.contains("dark") ? "dark" : "light"); 35 36 btn.addEventListener("click", () => { 37 const next = root.classList.contains("dark") ? "light" : "dark"; 38 localStorage.setItem("theme", next); 39 apply(next); 40 }); 41 })(); 42</script>
42 </div> </div> <nav class="sm:hidden border-t border-[color:var(--border)] text-sm"> <div class="container-wide flex items-center gap-1 overflow-x-auto py-2"> <a href="/" class="px-3 py-1 rounded-md no-underline whitespace-nowrap text-[color:var(--accent-strong)] bg-[color:var(--accent-soft)]"> Home </a><a href="/posts/" class="px-3 py-1 rounded-md no-underline whitespace-nowrap text-[color:var(--ink-soft)] hover:text-[color:var(--ink)]"> Posts </a><a href="/notes/" class="px-3 py-1 rounded-md no-underline whitespace-nowrap text-[color:var(--ink-soft)] hover:text-[color:var(--ink)]"> Notes </a><a href="/about/" class="px-3 py-1 rounded-md no-underline whitespace-nowrap text-[color:var(--ink-soft)] hover:text-[color:var(--ink)]"> About </a> </div> </nav> </header> <main class="flex-1" id="main"> <div class="relative bg-[color:var(--bg-soft)] border-b border-[color:var(--border)] overflow-hidden"> <div id="hero-mesh" class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true"></div>
42<script type="module" src="/_astro/HeroMesh.astro_astro_type_script_index_0_lang.Dd7fVhvF.js"></script>
42 <section class="container-wide py-12 sm:py-16 relative pointer-events-none hero-legible"> <h1 class="text-3xl sm:text-4xl font-semibold leading-tight tracking-tight mb-3"> 43Hi, I'm Marty. 44</h1> <p class="text-lg sm:text-xl text-[color:var(--ink-soft)] leading-relaxed max-w-2xl"> Things I make, learn, and think about. I write about software, hardware, AI, gaming, the homestead, and whatever else I'm tinkering with. 45</p> </section> </div> <section class="container-wide pt-16 pb-16"> <div class="flex items-baseline justify-between mb-2"> <h2 class="text-2xl font-semibold">Recent posts</h2> <a href="/posts/" class="text-sm">All posts â</a> </div> <div> <article class="group flex flex-col sm:flex-row gap-5 py-6 border-b border-[color:var(--border)] last:border-b-0"> <div class="min-w-0 flex-1"> <p class="text-xs uppercase tracking-wider text-[color:var(--ink-muted)] mb-1"> <time datetime="2026-09-21T00:00:00.000Z">21 September 2026</time> </p> <h2 class="text-xl sm:text-2xl font-semibold leading-tight mb-2"> <a href="/posts/how-i-build-nestjs-services/" class="no-underline text-[color:var(--ink)] hover:text-[color:var(--accent)]"> How I build Nest.js services </a> </h2> <p class="text-[color:var(--ink-soft)] leading-relaxed mb-3 line-clamp-3">Close to a decade and dozens of production services in: the project structure, dependency injection rules, test harness, consumers, migrations, auth and monitoring I start every Nest.js project with, plus the skills that let an agent verify its own work.</p> <ul class="tag-list flex flex-wrap gap-1.5 not-prose" data-collapsible data-astro-cid-lga65v7i><li data-astro-cid-lga65v7i><button type="button" data-search-tag="software" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag software" data-astro-cid-lga65v7i> 46#software</button></li><li class="tag-list__more" data-astro-cid-lga65v7i><button type="button" class="pill px-2 py-0.5 text-xs" aria-expanded="false" aria-label="Show 4 more tags" data-astro-cid-lga65v7i> 47+ 4 more 48</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="javascript" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag javascript" tabindex="-1" data-astro-cid-lga65v7i> 49#javascript</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="graphql" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag graphql" tabindex="-1" data-astro-cid-lga65v7i> 50#graphql</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="databases" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag databases" tabindex="-1" data-astro-cid-lga65v7i> 51#databases</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="ai" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag ai" tabindex="-1" data-astro-cid-lga65v7i> 52#ai</button></li></ul>
52<script type="module">document.querySelectorAll(".tag-list[data-collapsible]").forEach(e=>{const t=e.querySelector(".tag-list__more button");t&&t.addEventListener("click",a=>{a.stopPropagation(),e.hasAttribute("data-expanded")?(e.removeAttribute("data-expanded"),t.setAttribute("aria-expanded","false")):(e.setAttribute("data-expanded",""),t.setAttribute("aria-expanded","true"),e.querySelectorAll(".tag-list__hidden button").forEach(r=>r.removeAttribute("tabindex")))})});</script>
52 </div> </article><article class="group flex flex-col sm:flex-row gap-5 py-6 border-b border-[color:var(--border)] last:border-b-0"> <a href="/posts/coding-from-my-phone/" class="block shrink-0 sm:w-48 aspect-[16/10] overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--bg-soft)] no-underline"> <img src="/_astro/coding-from-my-phone-header.C6MaO2ZR_15tb34.webp" alt="A small black mini PC sitting on a slab of dark slate against a near-black background, with a ribbon of glowing orange light streaming out of its front ports and arcing up and away to the right, casting an amber reflection on the stone." loading="lazy" decoding="async" fetchpriority="auto" width="480" height="300" class="w-full h-full object-cover transition-transform group-hover:scale-[1.03]"> </a> <div class="min-w-0 flex-1"> <p class="text-xs uppercase tracking-wider text-[color:var(--ink-muted)] mb-1"> <time datetime="2026-08-17T00:00:00.000Z">17 August 2026</time> </p> <h2 class="text-xl sm:text-2xl font-semibold leading-tight mb-2"> <a href="/posts/coding-from-my-phone/" class="no-underline text-[color:var(--ink)] hover:text-[color:var(--accent)]"> Coding from my phone </a> </h2> <p class="text-[color:var(--ink-soft)] leading-relaxed mb-3 line-clamp-3">Terminus, Tailscale, and a NUC in the study. The setup I use to get real work done from a phone, and the two places it stops being pleasant.</p> <ul class="tag-list flex flex-wrap gap-1.5 not-prose" data-collapsible data-astro-cid-lga65v7i><li data-astro-cid-lga65v7i><button type="button" data-search-tag="software" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag software" data-astro-cid-lga65v7i> 53#software</button></li><li class="tag-list__more" data-astro-cid-lga65v7i><button type="button" class="pill px-2 py-0.5 text-xs" aria-expanded="false" aria-label="Show 3 more tags" data-astro-cid-lga65v7i> 54+ 3 more 55</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="ai" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag ai" tabindex="-1" data-astro-cid-lga65v7i> 56#ai</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="tools" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag tools" tabindex="-1" data-astro-cid-lga65v7i> 57#tools</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="hardware" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag hardware" tabindex="-1" data-astro-cid-lga65v7i> 58#hardware</button></li></ul> </div> </article><article class="group flex flex-col sm:flex-row gap-5 py-6 border-b border-[color:var(--border)] last:border-b-0"> <a href="/posts/ask-for-the-method-not-the-fix/" class="block shrink-0 sm:w-48 aspect-[16/10] overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--bg-soft)] no-underline"> <img src="/_astro/ask-for-the-method-header.BfV5yxvH_Z2XIo8.webp" alt="A dark stone basin brimming with glowing molten-orange light, overflowing down one edge in thin rivulets and pooling on the charcoal floor below. Faint circuit-board traces are etched into the basin's side." loading="lazy" decoding="async" fetchpriority="auto" width="480" height="300" class="w-full h-full object-cover transition-transform group-hover:scale-[1.03]"> </a> <div class="min-w-0 flex-1"> <p class="text-xs uppercase tracking-wider text-[color:var(--ink-muted)] mb-1"> <time datetime="2026-07-22T00:00:00.000Z">22 July 2026</time> </p> <h2 class="text-xl sm:text-2xl font-semibold leading-tight mb-2"> <a href="/posts/ask-for-the-method-not-the-fix/" class="no-underline text-[color:var(--ink)] hover:text-[color:var(--accent)]"> Ask for the method, not the fix </a> </h2> <p class="text-[color:var(--ink-soft)] leading-relaxed mb-3 line-clamp-3">Six months of confident, plausible, wrong fixes for the same Postgres incident â and what changed when I stopped asking Claude for a patch and asked it to teach me how to diagnose one.</p> <ul class="tag-list flex flex-wrap gap-1.5 not-prose" data-collapsible data-astro-cid-lga65v7i><li data-astro-cid-lga65v7i><button type="button" data-search-tag="software" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag software" data-astro-cid-lga65v7i>
59#software</button></li><li class="tag-list__more" data-astro-cid-lga65v7i><button type="button" class="pill px-2 py-0.5 text-xs" aria-expanded="false" aria-label="Show 3 more tags" data-astro-cid-lga65v7i> 60+ 3 more 61</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="ai" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag ai" tabindex="-1" data-astro-cid-lga65v7i> 62#ai</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="databases" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag databases" tabindex="-1" data-astro-cid-lga65v7i> 63#databases</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="tools" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag tools" tabindex="-1" data-astro-cid-lga65v7i> 64#tools</button></li></ul> </div> </article><article class="group flex flex-col sm:flex-row gap-5 py-6 border-b border-[color:var(--border)] last:border-b-0"> <a href="/posts/steam-machine-vs-a-build/" class="block shrink-0 sm:w-48 aspect-[16/10] overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--bg-soft)] no-underline"> <img src="/_astro/steam-machine-header.DVu4Duv1_Z2fL9TH.webp" alt="Three glowing orange bars of descending height, etched with faint circuit-board traces, standing against a dark charcoal background like a three-tier bar chart." loading="lazy" decoding="async" fetchpriority="auto" width="480" height="300" class="w-full h-full object-cover transition-transform group-hover:scale-[1.03]"> </a> <div class="min-w-0 flex-1"> <p class="text-xs uppercase tracking-wider text-[color:var(--ink-muted)] mb-1"> <time datetime="2026-07-03T00:00:00.000Z">3 July 2026</time> </p> <h2 class="text-xl sm:text-2xl font-semibold leading-tight mb-2"> <a href="/posts/steam-machine-vs-a-build/" class="no-underline text-[color:var(--ink)] hover:text-[color:var(--accent)]"> Steam Machine, or the box you'd build for the money </a> </h2> <p class="text-[color:var(--ink-soft)] leading-relaxed mb-3 line-clamp-3">Valve's Steam Machine landed in Australia at $1,609. I priced a cheaper build, a like-for-like one, and a higher-end one against it â and thanks to the RAM crisis, the usual just-build-one answer isn't the walkover it normally is.</p> <ul class="tag-list flex flex-wrap gap-1.5 not-prose" data-collapsible data-astro-cid-lga65v7i><li data-astro-cid-lga65v7i><button type="button" data-search-tag="hardware" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag hardware" data-astro-cid-lga65v7i> 65#hardware</button></li><li class="tag-list__more" data-astro-cid-lga65v7i><button type="button" class="pill px-2 py-0.5 text-xs" aria-expanded="false" aria-label="Show 1 more tag" data-astro-cid-lga65v7i> 66+ 1 more 67</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="gaming" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag gaming" tabindex="-1" data-astro-cid-lga65v7i> 68#gaming</button></li></ul> </div> </article><article class="group flex flex-col sm:flex-row gap-5 py-6 border-b border-[color:var(--border)] last:border-b-0"> <a href="/posts/everything-you-own-asks-for-something-back/" class="block shrink-0 sm:w-48 aspect-[16/10] overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--bg-soft)] no-underline"> <img src="/_astro/everything-you-own-asks-for-something-back-header.BWr4-vl3_21wlfg.webp" alt="A single matte grey sphere resting on a dark stone surface against a near-black background, lit from one side by a warm orange glow that catches its left edge. Vast empty negative space surrounds it." loading="lazy" decoding="async" fetchpriority="auto" width="480" height="300" class="w-full h-full object-cover transition-transform group-hover:scale-[1.03]"> </a> <div class="min-w-0 flex-1"> <p class="text-xs uppercase tracking-wider text-[color:var(--ink-muted)] mb-1"> <time datetime="2026-06-26T00:00:00.000Z">26 June 2026</time> </p> <h2 class="text-xl sm:text-2xl font-semibold leading-tight mb-2">
68 <a href="/posts/everything-you-own-asks-for-something-back/" class="no-underline text-[color:var(--ink)] hover:text-[color:var(--accent)]"> Everything you own asks for something back </a> </h2> <p class="text-[color:var(--ink-soft)] leading-relaxed mb-3 line-clamp-3">Not the one-plant, two-t-shirts kind of minimalism. A slower version built around a single idea â cost of ownership â and what changed once it clicked: a simpler wardrobe, a quieter desk, a streamlined digital life, and a noticeable weight off.</p> <ul class="tag-list flex flex-wrap gap-1.5 not-prose" data-collapsible data-astro-cid-lga65v7i><li data-astro-cid-lga65v7i><button type="button" data-search-tag="personal" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag personal" data-astro-cid-lga65v7i> 69#personal</button></li><li class="tag-list__more" data-astro-cid-lga65v7i><button type="button" class="pill px-2 py-0.5 text-xs" aria-expanded="false" aria-label="Show 1 more tag" data-astro-cid-lga65v7i> 70+ 1 more 71</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="minimalism" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag minimalism" tabindex="-1" data-astro-cid-lga65v7i> 72#minimalism</button></li></ul> </div> </article> </div> </section> <section class="container-wide pb-20"> <div class="flex items-baseline justify-between mb-2"> <h2 class="text-2xl font-semibold">Recent notes</h2> <a href="/notes/" class="text-sm">All notes â</a> </div> <div> <article class="py-6 border-b border-[color:var(--border)] last:border-b-0"> <div class="prose prose-sm max-w-none"> <p>TIL that <code>CREATE TEMPORARY TABLE</code> in Postgres takes an <code>ON COMMIT</code> clause:</p> 73<pre class="astro-code astro-code-themes vitesse-light vitesse-dark" style="background-color:#ffffff;--shiki-dark-bg:#121212;color:#393a34;--shiki-dark:#dbd7caee; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="sql"><code><span class="line"><span style="color:#1E754F;--shiki-dark:#4D9375">CREATE</span><span style="color:#393A34;--shiki-dark:#DBD7CAEE"> TEMP </span><span style="color:#1E754F;--shiki-dark:#4D9375">TABLE</span><span style="color:#393A34;--shiki-dark:#DBD7CAEE"> staging (id </span><span style="color:#AB5959;--shiki-dark:#CB7676">bigint</span><span style="color:#393A34;--shiki-dark:#DBD7CAEE">, payload jsonb) </span><span style="color:#1E754F;--shiki-dark:#4D9375">ON</span><span style="color:#1E754F;--shiki-dark:#4D9375"> COMMIT</span><span style="color:#1E754F;--shiki-dark:#4D9375"> DROP</span><span style="color:#393A34;--shiki-dark:#DBD7CAEE">;</span></span></code></pre> 74<p>Three settings. <code>PRESERVE ROWS</code> is the default and keeps the table around for 75the whole session. <code>DELETE ROWS</code> truncates it at every commit. <code>DROP</code> bins the 76table itself at the end of the transaction that created it, which is almost 77always what I actually wanted â a scratch table for one bulk load or one gnarly 78multi-step query, gone the moment Iâm done with it. No cleanup step, no 79<code>DROP TABLE IF EXISTS</code> at the top guarding against whatever the last run left 80lying around.</p> 81<p>Postgres deviates from the standard here, incidentally: SQL says the default 82should be <code>DELETE ROWS</code>, and Postgres went with <code>PRESERVE ROWS</code>.</p> 83<p>The thing that made it click was PgBouncerâs feature map, which lists 84<code>ON COMMIT DROP</code> temp tables as working under transaction pooling and 85<code>PRESERVE/DELETE ROWS</code> as âNeverâ. Of course it does â in transaction pooling 86the server connection goes back in the pool after each commit, so anything 87session-scoped either leaks into someone elseâs transaction or vanishes. 88Scope the table to the transaction and thereâs nothing left to leak.</p> </div> <footer class="mt-4 flex items-center justify-between gap-4 text-xs text-[color:var(--ink-muted)]"> <div class="min-w-0"> <ul class="tag-list flex flex-wrap gap-1.5 not-prose" data-collapsible data-astro-cid-lga65v7i><li data-astro-cid-lga65v7i><button type="button" data-search-tag="software" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag software" data-astro-cid-lga65v7i>
89#software</button></li><li class="tag-list__more" data-astro-cid-lga65v7i><button type="button" class="pill px-2 py-0.5 text-xs" aria-expanded="false" aria-label="Show 1 more tag" data-astro-cid-lga65v7i> 90+ 1 more 91</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="databases" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag databases" tabindex="-1" data-astro-cid-lga65v7i> 92#databases</button></li></ul> </div> <a href="/notes/postgres-on-commit-drop/" class="shrink-0 no-underline text-[color:var(--ink-muted)] hover:text-[color:var(--accent)]" aria-label="Permalink to note from 3 September 2026"> <time datetime="2026-09-03T17:04:00.000Z">3 September 2026</time> <span class="ml-1.5" aria-hidden="true">#</span> </a> </footer> </article><article class="py-6 border-b border-[color:var(--border)] last:border-b-0"> <div class="prose prose-sm max-w-none"> <p>Threw together <a href="https://relink.martyw.dev/" rel="noopener noreferrer" target="_blank">Relink Codex</a> â playstyles, combo 93routes, weapons and sigil loadouts for all 28 characters in <em>Granblue Fantasy: 94Relink</em>.</p> 95<p>Two things collided. The expansion pulled me back into the game, Iâm somewhere 96around Proud and trying to learn characters Iâve never touched, and the usual way 97to do that is trawling Reddit threads and pausing YouTube videos on the frame 98where the actual information is. I didnât fancy it. The other thing is that I pay 99for Claude Max every month and reliably donât get near the token allowance, which 100is its own quiet kind of waste â the plan costs the same whether I spend it or 101not.</p> 102<p>So the site is less a project than the output of a research task I didnât want to 103do by hand. Three power brackets rather than one canonical build, because a 104loadout thatâs right at 12,000 PWR is wrong by the time Endless Ragnarok has you 105at 40,000. Astro, static, no backend.</p> 106<p>Itâs current as of patch 2.0.2 and will quietly stop being true the moment 107Cygames ships another one. But re-running the research costs me nothing I wasnât 108already paying, which was most of the point.</p> </div> <footer class="mt-4 flex items-center justify-between gap-4 text-xs text-[color:var(--ink-muted)]"> <div class="min-w-0"> <ul class="tag-list flex flex-wrap gap-1.5 not-prose" data-collapsible data-astro-cid-lga65v7i><li data-astro-cid-lga65v7i><button type="button" data-search-tag="gaming" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag gaming" data-astro-cid-lga65v7i> 109#gaming</button></li><li class="tag-list__more" data-astro-cid-lga65v7i><button type="button" class="pill px-2 py-0.5 text-xs" aria-expanded="false" aria-label="Show 3 more tags" data-astro-cid-lga65v7i> 110+ 3 more 111</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="web" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag web" tabindex="-1" data-astro-cid-lga65v7i> 112#web</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="software" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag software" tabindex="-1" data-astro-cid-lga65v7i> 113#software</button></li><li class="tag-list__hidden" data-astro-cid-lga65v7i><button type="button" data-search-tag="ai" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag ai" tabindex="-1" data-astro-cid-lga65v7i> 114#ai</button></li></ul> </div> <a href="/notes/relink-codex/" class="shrink-0 no-underline text-[color:var(--ink-muted)] hover:text-[color:var(--accent)]" aria-label="Permalink to note from 5 August 2026"> <time datetime="2026-08-05T20:50:00.000Z">5 August 2026</time> <span class="ml-1.5" aria-hidden="true">#</span> </a> </footer> </article><article class="py-6 border-b border-[color:var(--border)] last:border-b-0"> <div class="prose prose-sm max-w-none"> <p>Summer Game Fest dropped on Friday and somehow the lineup read like it was 115scraped straight from my wishlist. Three of my favourite franchises, all in one 116show, all within a couple of hours of each other. I keep waiting for the cat
116ch.</p> 117<p>Code Veronica is finally getting the remake treatment â retitled just <em>Resident 118Evil: Veronica</em>, Claire back as the lead, the reveal opening on a rainy Paris 119apartment block. Itâs been the obvious gap in Capcomâs remake run for years and 120theyâve finally filled it. 2027, and on Switch 2 alongside the usual platforms.</p> 121<p>Then FF7 Remake Part 3 turned out to be <em>Final Fantasy 7 Revelation</em> â Hamaguchi 122on stage, the Highwind in your hands, skydiving into an open world, job classes 123in combat. Spring 2027. The trilogy actually lands.</p> 124<p>And the one I didnât see coming: <em>Stellar Blade: Blood Rain</em>. New protagonist â 125Evie, not Eve â swapping the sword for gauntlet brawling, and Shift Up 126self-publishing this time so itâs not a PlayStation exclusive. Still early, but 127pointed at 2027 too.</p> 128<p>So thatâs my three most-anticipated games sharing a release year and a single 129showcase. Either the universe owes me one or 2027 is going to cost me a fortune.</p> </div> <footer class="mt-4 flex items-center justify-between gap-4 text-xs text-[color:var(--ink-muted)]"> <div class="min-w-0"> <ul class="tag-list flex flex-wrap gap-1.5 not-prose" data-astro-cid-lga65v7i><li data-astro-cid-lga65v7i><button type="button" data-search-tag="gaming" class="pill px-2 py-0.5 text-xs" aria-label="Search for tag gaming" data-astro-cid-lga65v7i> 130#gaming</button></li></ul> </div> <a href="/notes/summer-game-fest-2026-perfect-lineup/" class="shrink-0 no-underline text-[color:var(--ink-muted)] hover:text-[color:var(--accent)]" aria-label="Permalink to note from 7 June 2026"> <time datetime="2026-06-07T20:09:00.000Z">7 June 2026</time> <span class="ml-1.5" aria-hidden="true">#</span> </a> </footer> </article> </div> </section> </main> <footer class="border-t border-[color:var(--border)] mt-16 py-10 text-sm text-[color:var(--ink-muted)]"> <div class="container-wide flex flex-col sm:flex-row justify-between gap-4"> <p>© 2026 Marty.</p> <p class="flex gap-4"> <a href="/rss.xml">RSS</a> </p> </div> </footer> <dialog id="search-dialog" class="w-[min(92vw,560px)] rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-0 shadow-2xl [margin:10vh_auto_auto] backdrop:bg-black/40 text-[color:var(--ink)]" aria-label="Search posts and notes" data-astro-cid-euff75kw> <div class="flex items-center gap-2 border-b border-[color:var(--border)] px-4" data-astro-cid-euff75kw> <svg class="w-4 h-4 flex-none text-[color:var(--ink-muted)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-euff75kw> <circle cx="11" cy="11" r="7" data-astro-cid-euff75kw></circle> <path d="m20 20-3.5-3.5" data-astro-cid-euff75kw></path> </svg> <input id="search-input" type="text" autocomplete="off" spellcheck="false" placeholder="Search posts and notesâ¦" aria-label="Search posts and notes" aria-controls="search-results" class="w-full bg-transparent py-3 text-[0.95rem] text-[color:var(--ink)] outline-none placeholder:text-[color:var(--ink-muted)]" data-astro-cid-euff75kw> <kbd class="hidden sm:inline-flex flex-none rounded border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-1.5 py-0.5 font-mono text-[0.7rem] text-[color:var(--ink-muted)]" data-astro-cid-euff75kw> 131ESC 132</kbd> </div> <div id="search-tags" class="flex flex-wrap gap-1.5 border-b border-[color:var(--border)] px-3 py-2" role="group" aria-label="Filter by tag" data-astro-cid-euff75kw> <button type="button" data-tag="software" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#software</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>19</span> </button><button type="button" data-tag="web" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#web</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>12</span> </button><button type="button" data-tag="javascript" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#javascript</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>8</span> </button><button type="button" data-tag="ai" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#ai</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>7</span> </button><button type="button" data-tag="gaming" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#gaming</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>6</span> </button><button type="button" data-tag="tools" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#tools</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>6</span> </button><button type="button" data-tag="apis" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#apis</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>5</span> </button><button type="button" data-tag="meta" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#meta</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>5</span> </button><button type="button" data-tag="personal" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#personal</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>4</span> </button><button type="button" data-tag="databases" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#databases</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>
1323</span> </button><button type="button" data-tag="communication" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#communication</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>2</span> </button><button type="button" data-tag="graphql" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#graphql</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>2</span> </button><button type="button" data-tag="hardware" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#hardware</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>2</span> </button><button type="button" data-tag="books" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#books</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>1</span> </button><button type="button" data-tag="finance" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#finance</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>1</span> </button><button type="button" data-tag="homestead" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#homestead</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>1</span> </button><button type="button" data-tag="minimalism" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#minimalism</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>1</span> </button><button type="button" data-tag="showerthought" class="search-tag-btn inline-flex items-center gap-1 rounded-full border border-[color:var(--border)] bg-[color:var(--bg-soft)] px-2 py-0.5 text-[0.7rem] text-[color:var(--ink-soft)] hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] transition-colors" data-astro-cid-euff75kw> <span data-astro-cid-euff75kw>#showerthought</span> <span class="text-[color:var(--ink-muted)]" data-astro-cid-euff75kw>1</span> </button> </div> <ul id="search-results" role="listbox" aria-label="Search results" class="m-0 max-h-[58vh] list-none overflow-y-auto p-2 not-prose" data-astro-cid-euff75kw></ul> <p id="search-empty" class="hidden px-4 py-8 text-center text-sm text-[color:var(--ink-muted)]" data-astro-cid-euff75kw> 133No matches. 134</p> </dialog>
134<script type="application/json" id="search-data">[{"k":"post","t":"How I build Nest.js services","s":"Close to a decade and dozens of production services in: the project structure, dependency injection rules, test harness, consumers, migrations, auth and monitoring I start every Nest.js project with, ","u":"/posts/how-i-build-nestjs-services/","d":"21 Sept 2026","g":["software","javascript","graphql","databases","ai"]},{"k":"post","t":"Coding from my phone","s":"Terminus, Tailscale, and a NUC in the study. The setup I use to get real work done from a phone, and the two places it stops being pleasant.","u":"/posts/coding-from-my-phone/","d":"17 Aug 2026","g":["software","ai","tools","hardware"]},{"k":"post","t":"Ask for the method, not the fix","s":"Six months of confident, plausible, wrong fixes for the same Postgres incident â and what changed when I stopped asking Claude for a patch and asked it to teach me how to diagnose one.","u":"/posts/ask-for-the-method-not-the-fix/","d":"22 July 2026","g":["software","ai","databases","tools"]},{"k":"post","t":"Steam Machine, or the box you'd build for the money","s":"Valve's Steam Machine landed in Australia at $1,609. I priced a cheaper build, a like-for-like one, and a higher-end one against it â and thanks to the RAM crisis, the usual just-build-one answer isn'","u":"/posts/steam-machine-vs-a-build/","d":"3 July 2026","g":["hardware","gaming"]},{"k":"post","t":"Everything you own asks for something back","s":"Not the one-plant, two-t-shirts kind of minimalism. A slower version built around a single idea â cost of ownership â and what changed once it clicked: a simpler wardrobe, a quieter desk, a streamline","u":"/posts/everything-you-own-asks-for-something-back/","d":"26 June 2026","g":["personal","minimalism"]},{"k":"post","t":"Your values aren't the ones you'd list","s":"Most of what people call their values â being active, a big social circle, a good career â are really social idealisms: the life they think they should want. A short take on the distinction John DeMar","u":"/posts/social-idealisms-arent-values/","d":"21 June 2026","g":["personal","books"]},{"k":"post","t":"Raise your hand first","s":"Owning a mistake looks like it should cost you standing. Done consistently and meant, it does the opposite â here's the outage that taught me why, and the mechanism underneath it.","u":"/posts/raise-your-hand-first/","d":"19 June 2026","g":["personal","communication"]},{"k":"post","t":"The bill for being direct","s":"Most people who call themselves brutally honest just can't read the room. Being direct when you can read it â and saying the true thing anyway â is a different trade, and the bill comes due differentl","u":"/posts/the-bill-for-being-direct/","d":"12 June 2026","g":["personal","communication"]},{"k":"post","t":"Stable URLs without a redirect table","s":"A short random prefix plus a throwaway slug gives you URLs that survive title changes â no UUID ugliness, no slug-history table. Plus why the collision-retry loop matters more than the keyspace sugges","u":"/posts/stable-urls-without-a-redirect-table/","d":"1 June 2026","g":["software","web","apis"]},{"k":"post","t":"How this blog gets written","s":"How posts and notes here actually get made â AI-assisted, with a structured pass that flags speculation rather than laundering it as fact.","u":"/posts/how-this-blog-gets-written/","d":"29 May 2026","g":["meta","ai","tools"]},{"k":"post","t":"My 2026 Tech Stack","s":"Two years on from the 2024 list. The shipping-code part has changed completely; most of the rest looks the same â and where it doesn't, the reasons are worth knowing.","u":"/posts/my-2026-tech-stack/","d":"28 May 2026","g":["software","web","ai","meta","tools"]},{"k":"post","t":"The rent-vs-buy spreadsheet I finally closed","s":"For a couple of years I was sure that renting and tipping the difference into index funds beat buying. Then I bought a smaller, older house on a worse street â and worked out what the spreadsheet had ","u":"/posts/rent-vs-buy-spreadsheet/","d":"20 May 2026","g":["finance","homestead"]},{"k":"post","t":"Promise.all isn't batching","s":"When parallelising N small queries looks like a fix and is really just self-DoS at a slightly higher concurrency level.","u":"/posts/promise-all-isnt-batching/","d":"26 Aug 2025","g":["software","javascript","web","apis"]},{"k":"post","t":"Write batch-shaped functions by default","s":"Why I write repository methods that take arrays and return Maps, even when the only caller â today â is asking for one thing.","u":"/posts/write-batch-shaped-functions-by-default/","d":"12 Aug 2025","g":["software","javascript","apis"]},{"k":"post","t":"Batching requests we don't control","s":"How pushing dataloader up to the HTTP controller â out of the resolver layer it usually lives in â cut downstream database load by 83% on a read-heavy endpoint.","u":"/posts/batching-requests-we-dont-control/","d":"7 Mar 2025","g":["software","web","javascript","apis"]},{"k":"post","t":"My 2024 Tech Stack","s":"What I'm reaching for this year â languages, frameworks, infra â and why.","u":"/posts/my-2024-tech-stack/","d":"7 Mar 2024","g":["software","web","meta","tools"]},{"k":"post","t":"JavaScript Game Development Engine Ideas","s":"Where the JavaScript game-engine landscape has gaps, and how I'd fill them if I built one.","u":"/posts/javascript-game-development-engine-ideas/","d":"4 Mar 2024","g":["software","gam
134ing","javascript","web"]},{"k":"post","t":"Thoughts on GraphQL After 2 Years","s":"What two years of running a production GraphQL service taught me about where it earns its keep and where it doesn't.","u":"/posts/thoughts-on-graphql-after-2-years/","d":"21 Nov 2022","g":["software","graphql","web","apis"]},{"k":"note","t":"TIL that CREATE TEMPORARY TABLE in Postgres takes an ON COMMIT clause: Three settings. PRESERVE ROWS is the default and ","s":"TIL that CREATE TEMPORARY TABLE in Postgres takes an ON COMMIT clause: Three settings. PRESERVE ROWS is the default and keeps the table around for the whole session. DELETE ROWS truncates it at every commit. DROP bins the table itself at the end of the transaction that created it, which is almost always what I actually wanted â a scratch table for one bulk load or one gnarly multi step query, gone","u":"/notes/postgres-on-commit-drop/","d":"3 Sept 2026","g":["software","databases"]},{"k":"note","t":"Threw together Relink Codex â playstyles, combo routes, weapons and sigil loadouts for all 28 characters in Granblue Fan","s":"Threw together Relink Codex â playstyles, combo routes, weapons and sigil loadouts for all 28 characters in Granblue Fantasy: Relink . Two things collided. The expansion pulled me back into the game, I'm somewhere around Proud and trying to learn characters I've never touched, and the usual way to do that is trawling Reddit threads and pausing YouTube videos on the frame where the actual informati","u":"/notes/relink-codex/","d":"5 Aug 2026","g":["gaming","web","software","ai"]},{"k":"note","t":"Summer Game Fest dropped on Friday and somehow the lineup read like it was scraped straight from my wishlist. Three of m","s":"Summer Game Fest dropped on Friday and somehow the lineup read like it was scraped straight from my wishlist. Three of my favourite franchises, all in one show, all within a couple of hours of each other. I keep waiting for the catch. Code Veronica is finally getting the remake treatment â retitled just Resident Evil: Veronica , Claire back as the lead, the reveal opening on a rainy Paris apartmen","u":"/notes/summer-game-fest-2026-perfect-lineup/","d":"7 June 2026","g":["gaming"]},{"k":"note","t":"Spent the week deciding on a physics engine for the game engine and landed on Rapier. It's written in Rust and ships to ","s":"Spent the week deciding on a physics engine for the game engine and landed on Rapier. It's written in Rust and ships to the browser as WASM, which sounds like overkill for a 2D hobby engine until you watch the alternatives fall over. PhysicsJS was the first I crossed off â last meaningful release was years ago, and I'm not building on something nobody's touched since the previous decade. Matter.js","u":"/notes/rapier-for-the-game-engine/","d":"4 June 2026","g":["software","gaming","javascript","web"]},{"k":"note","t":"The comments on here are down right now. They run on Cusdis â open source, on their free hosted tier, and the most light","s":"The comments on here are down right now. They run on Cusdis â open source, on their free hosted tier, and the most lightweight option I could find. I've used a few commenting systems over the years â FastComments, Disqus, that lot â and Cusdis was the leanest by a distance: no tracker payload, no account wall, just a textbox. The catch with lightweight and free is that when the hosted instance fal","u":"/notes/comments-are-down-thats-the-deal/","d":"1 June 2026","g":["meta","software","web","tools"]},{"k":"note","t":"Two years after writing up some ideas for a JavaScript game engine, I've finally started building it. Early days, but it","s":"Two years after writing up some ideas for a JavaScript game engine, I've finally started building it. Early days, but it lives at arcade2d.com.","u":"/notes/arcade2d-finally-started/","d":"28 May 2026","g":["software","gaming","javascript","web"]},{"k":"note","t":"New site, new shape. The old one was a developer CV with blog. This one is broader â software, hardware, AI, the homeste","s":"New site, new shape. The old one was a developer CV with blog. This one is broader â software, hardware, AI, the homestead, gaming, the occasional showerthought. Short stuff lives here in Notes; longer pieces under Posts. Built with Astro and Tailwind on Cloudflare Workers. Most of the wiring â components, porting old articles, even the voice this note is written in â went through Claude Code. Wor","u":"/notes/new-site-new-shape/","d":"28 May 2026","g":["meta","ai","showerthought"]},{"k":"note","t":"The instinct when something's slow is to put Redis in front of it. Sometimes that's right. Often it's a way to defer thi","s":"The instinct when something's slow is to put Redis in front of it. Sometimes that's right. Often it's a way to defer thinking about the actual problem, with the added bonus of now having two: the original slowness plus a cache that drifts out of sync with the truth in ways you can't predict. The question that filters most \"let's add a cache\" proposals: how does it get invalidated? If the answer is","u":"/notes/caches-need-an-invalidation-story/","d":"29 Aug 2025","g":["software","web"]},{"k":"note","t":"A common shape: fetch a flat list of rows, then group them in application code into a Map<parentId, Row[] before returni","s":"A common shape: fetch a flat list of rows, then group them in application code into a Map<parentId, Row[] before returning. It works, it's familiar, and it's nearly always slower and more memory hungry than asking the database to do it. One row per parent, the database handles the grouping, and the type cast keeps the application honest about what it's getting back. MySQL has GROUP CONCAT with the","u":"/notes/group-in-the-database/","d":"19 Aug 2025","g":["software"]},{"k":"note","t":"The dataloader package ships with caching on. For the GraphQL resolver tree it was designed for that makes sense â stabl","s":"The dataloader package ships with caching on. For the GraphQL resolver tree it was designed for that makes sense â stable reads within a single request are what you want, and the loader is garbage collected with the request. Reuse a loader across requests or hold one for the lifetime of a server though and the cache becomes a Map that grows forever, never evicts, and quietly hands out stale reads.","u":"/notes/dataloader-cache-off-by-default/","d":"6 Aug 2025","g":["software","javascript"]}]</script>
134
134<script type="module">const l=document.getElementById("search-dialog"),o=document.getElementById("search-input"),v=document.getElementById("search-results"),k=document.getElementById("search-empty"),w=document.getElementById("search-data"),C=document.getElementById("search-tags");if(l&&o&&v&&k&&w){const L=JSON.parse(w.textContent??"[]");let c=[],i=0;const r=new Set,g=()=>{C?.querySelectorAll(".search-tag-btn").forEach(e=>{const t=e.dataset.tag??"";e.setAttribute("aria-pressed",String(r.has(t)))})},x=e=>{r.has(e)?r.delete(e):r.add(e),g()},b=e=>{r.clear();for(const t of e)r.add(t);g()},I=(e,t)=>{const s=e.t.toLowerCase();if(s===t)return 0;if(s.startsWith(t))return 1;const n=s.indexOf(t);return n>=0?2+n/1e3:e.g.join(" ").includes(t)?30:e.s.toLowerCase().includes(t)?50:1/0},A=e=>{const t=e.trim().toLowerCase();let s=L;return r.size>0&&(s=s.filter(n=>[...r].every(a=>n.g.includes(a)))),t?s.map(n=>({it:n,s:I(n,t)})).filter(n=>n.s!==1/0).sort((n,a)=>n.s-a.s||n.it.t.localeCompare(a.it.t)).map(n=>n.it):s.slice(0,20)},B=(e,t,s)=>{const n=document.createElement("li");n.setAttribute("role","option");const a=document.createElement("a");a.href=e.u,a.dataset.index=String(s),a.className="flex items-center gap-3 rounded-md px-3 py-2 no-underline hover:no-underline";const E=document.createElement("span");E.className="flex-none rounded border border-[color:var(--border)] px-1.5 py-0.5 text-[0.6rem] font-semibold uppercase tracking-[0.06em] text-[color:var(--ink-muted)]",E.textContent=e.k;const d=document.createElement("span");d.className="flex-1 truncate text-[color:var(--ink)]";const D=e.t.toLowerCase(),u=t?D.indexOf(t):-1;if(u>=0&&t){d.append(e.t.slice(0,u));const y=document.createElement("span");y.className="underline decoration-2 underline-offset-2 text-[color:var(--accent)]",y.textContent=e.t.slice(u,u+t.length),d.append(y,e.t.slice(u+t.length))}else d.textContent=e.t;const h=document.createElement("span");return h.className="ml-auto flex-none text-xs text-[color:var(--ink-muted)]",h.textContent=e.d,a.append(E,d,h),n.append(a),n},m=e=>{c.length!==0&&(i=(e+c.length)%c.length,v.querySelectorAll("a[data-index]").forEach((t,s)=>{const n=s===i;t.classList.toggle("bg-[color:var(--accent-soft)]",n),t.setAttribute("aria-selected",String(n)),n&&t.scrollIntoView({block:"nearest"})}))},S=e=>{v.replaceChildren(...c.map((t,s)=>B(t,e,s))),k.classList.toggle("hidden",c.length>0),i=0,m(0)},f=()=>{c=A(o.value),S(o.value.trim().toLowerCase())},p=(e=null)=>{const t=l.open;t?e&&x(e):(o.value="",b(e?[e]:[])),f(),t?o.focus():(l.showModal(),requestAnimationFrame(()=>o.focus()))};o.addEventListener("input",f),o.addEventListener("keydown",e=>{if(e.key==="ArrowDown")e.preventDefault(),m(i+1);else if(e.key==="ArrowUp")e.preventDefault(),m(i-1);else if(e.key==="Enter"){e.preventDefault();const t=c[i];t&&window.location.assign(t.u)}else if(e.key==="Backspace"&&!o.value&&r.size>0){e.preventDefault();const t=[...r].pop();r.delete(t),g(),f()}}),C?.addEventListener("click",e=>{const t=e.target.closest(".search-tag-btn");t?.dataset.tag&&(x(t.dataset.tag),f(),o.focus())}),l.addEventListener("click",e=>{e.target===l&&l.close()});const T=e=>{if(!(e instanceof HTMLElement))return!1;if(e.isContentEditable)return!0;const t=e.tagName;return t==="INPUT"||t==="TEXTAREA"||t==="SELECT"};document.addEventListener("keydown",e=>{if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==="k"){e.preventDefault(),p();return}e.key==="/"&&!e.metaKey&&!e.ctrlKey&&!e.altKey&&!l.open&&!T(e.target)&&(e.preventDefault(),p())}),document.addEventListener("click",e=>{const t=e.target.closest("[data-search-open], [data-search-tag]");if(!t)return;e.preventDefault();const s=t.dataset.searchTag?.toLowerCase()??null;p(s)})}</script>
134
134<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"5f2b6111917a431ea563c568bb099c6f","r":1,"spa":2}' crossorigin="anonymous"></script>
134 135</body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.