Files
nexus/sreweekly/articles/487/05-practical-problems-with-auto-increment.html
2026-09-12 17:23:01 +08:00

540 lines
30 KiB
HTML

<!DOCTYPE html>
<html class="has-newsletter-footer" lang="en">
<head>
<meta charset="utf-8">
<meta name="HandheldFriendly" content="True">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#f5f5f4">
<meta name="referrer" content="no-referrer-when-downgrade">
<meta name="format-detection" content="telephone=no">
<link rel="me" href="https://octodon.social/@samwho">
<link rel="me" href="https://hachyderm.io/@samwho">
<title>Practical Problems with Auto-Increment</title>
<link rel="stylesheet" href="/css/reset.css?h=6f7861e8f1e8cd8577de">
<link rel="stylesheet" href="/giallo.css?h=314b13baed710ac3980f">
<link rel="stylesheet" href="/css/main.css?h=c23105589554dab2cb41">
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml">
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content="@samwhoo" />
<meta name="twitter:image" content="/images/samwho-2018-banner.png" />
<meta name="twitter:image:alt" content="Samwho keyboard logo" />
<meta property="og:image" content="/images/samwho-2018-banner.png" />
<meta property="og:image:alt" content="Samwho keyboard logo" />
<meta property="og:image:width" content="2560" />
<meta property="og:image:height" content="1440" />
<meta name="twitter:description" content="Showing 2 real problems, with commands you can run to see them for yourself, that can crop up with using auto-increment fields in MySQL and Postgres">
<meta property="og:description" content="Showing 2 real problems, with commands you can run to see them for yourself, that can crop up with using auto-increment fields in MySQL and Postgres">
<meta name="description" content="Showing 2 real problems, with commands you can run to see them for yourself, that can crop up with using auto-increment fields in MySQL and Postgres">
<meta name="twitter:title" content="Practical Problems with Auto-Increment" />
<meta property="og:title" content="Practical Problems with Auto-Increment" />
<meta property="og:type" content="article" />
<meta property="og:published_date" content="2023-03-25" />
<meta property="og:author" content="Sam Rose" />
<script>
const url = new URL(window.location.href);
const params = url.searchParams;
const theme = params.get("theme") === "dark" ? "dark" : "light";
document.documentElement.dataset.theme = theme;
const hasNewsletterFooter =
document.documentElement.classList.contains("has-newsletter-footer");
document.querySelector('meta[name="theme-color"]').content =
theme === "dark"
? hasNewsletterFooter ? "#292524" : "#0c0a09"
: hasNewsletterFooter ? "#f5f5f4" : "#ffffff";
</script>
</head>
<body>
<div class="page-canvas">
<div class="bar" role="presentation">
<div class="segment"></div>
<div class="segment"></div>
<div class="segment"></div>
<div class="segment"></div>
<div class="segment"></div>
</div>
<a href="#content" class="skip-link">Skip to main content</a>
<header class="post-header">
<a href="/">
<img class="hero" alt="Samwho home" src="/images/samwho-keyslogo.svg" width="750" height="700" fetchpriority="high" />
</a>
<h1>Practical Problems with Auto-Increment</h1>
<div class="post-meta">
<time datetime="2023-03-25">2023-03-25</time>
</div>
</header>
<main id="content" tabindex="-1">
<article class="post">
<div class="content">
<p>In this post I'm going to demonstrate 2 reasons I will be avoiding
auto-increment fields in Postgres and MySQL in future. I'm going to prefer using
UUID fields unless I have a <em>very</em> good reason not to.</p>
<h2 id="mysql-8-0-auto-increment-id-re-use"><a class="anchor" href="#mysql-8-0-auto-increment-id-re-use" aria-label="Link to this section">#</a>
MySQL &lt;8.0 auto-increment ID re-use</h2>
<p>If you're running an older version of MySQL, it's possible for auto-incrementing
IDs to get re-used. Let's see this in action.</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>$ docker volume create mysql-data</span></span>
<span class="giallo-l"><span>$ docker run --platform linux/amd64 -e MYSQL_ROOT_PASSWORD=my-secret-pw -p 3306:3306 -v mysql-data:/var/lib/mysql mysql:5.7</span></span></code></pre>
<p>This gets us a Docker container of MySQL 5.7 running, attached to a volume that
will persist the data between runs of this container. Next let's get a simple
schema we can work with:</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>$ docker run -it --rm --network host --platform linux/amd64 mysql:5.7 mysql -h 127.0.0.1 -P 3306 -u root -p</span></span>
<span class="giallo-l"><span>mysql&gt; CREATE DATABASE my_database;</span></span>
<span class="giallo-l"><span>Query OK, 1 row affected (0.01 sec)</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>mysql&gt; USE my_database;</span></span>
<span class="giallo-l"><span>Database changed</span></span>
<span class="giallo-l"><span>mysql&gt; CREATE TABLE my_table (</span></span>
<span class="giallo-l"><span> -&gt; ID INT AUTO_INCREMENT PRIMARY KEY</span></span>
<span class="giallo-l"><span> -&gt; );</span></span>
<span class="giallo-l"><span>Query OK, 0 rows affected (0.02 sec)</span></span></code></pre>
<p>Now let's insert a couple of rows.</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>mysql&gt; INSERT INTO my_table () VALUES ();</span></span>
<span class="giallo-l"><span>Query OK, 1 row affected (0.03 sec)</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>mysql&gt; INSERT INTO my_table () VALUES ();</span></span>
<span class="giallo-l"><span>Query OK, 1 row affected (0.01 sec)</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>mysql&gt; INSERT INTO my_table () VALUES ();</span></span>
<span class="giallo-l"><span>Query OK, 1 row affected (0.01 sec)</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>mysql&gt; SELECT * FROM my_table;</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>| ID |</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>| 1 |</span></span>
<span class="giallo-l"><span>| 2 |</span></span>
<span class="giallo-l"><span>| 3 |</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>3 rows in set (0.01 sec)</span></span></code></pre>
<p>So far so good. We can restart the MySQL server and run the same SELECT
statement again and get the same result.</p>
<p>Let's delete a row.</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>mysql&gt; DELETE FROM my_table WHERE ID=3;</span></span>
<span class="giallo-l"><span>Query OK, 1 row affected (0.03 sec)</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>mysql&gt; SELECT * FROM my_table;</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>| ID |</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>| 1 |</span></span>
<span class="giallo-l"><span>| 2 |</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>2 rows in set (0.00 sec)</span></span></code></pre>
<p>Let's insert a new row to make sure the ID 3 doesn't get reused.</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>mysql&gt; INSERT INTO my_table () VALUES ();</span></span>
<span class="giallo-l"><span>Query OK, 1 row affected (0.02 sec)</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>mysql&gt; SELECT * FROM my_table;</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>| ID |</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>| 1 |</span></span>
<span class="giallo-l"><span>| 2 |</span></span>
<span class="giallo-l"><span>| 4 |</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>3 rows in set (0.00 sec)</span></span></code></pre>
<p>Perfect. Let's delete that latest row, restart the server, and then insert
a new row.</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>mysql&gt; DELETE FROM my_table WHERE ID=4;</span></span>
<span class="giallo-l"><span>Query OK, 1 row affected (0.01 sec)</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>mysql&gt; SELECT * FROM my_table;</span></span>
<span class="giallo-l"><span>ERROR 2013 (HY000): Lost connection to MySQL server during query</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>$ docker run -it --rm --network host --platform linux/amd64 mysql:5.7 mysql -h 127.0.0.1 -P 3306 -u root -p</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>mysql&gt; USE my_database;</span></span>
<span class="giallo-l"><span>Database changed</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>mysql&gt; SELECT * FROM my_table;</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>| ID |</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>| 1 |</span></span>
<span class="giallo-l"><span>| 2 |</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>2 rows in set (0.00 sec)</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>mysql&gt; INSERT INTO my_table () VALUES ();</span></span>
<span class="giallo-l"><span>Query OK, 1 row affected (0.03 sec)</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>mysql&gt; SELECT * FROM my_table;</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>| ID |</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>| 1 |</span></span>
<span class="giallo-l"><span>| 2 |</span></span>
<span class="giallo-l"><span>| 3 |</span></span>
<span class="giallo-l"><span>+----+</span></span>
<span class="giallo-l"><span>3 rows in set (0.00 sec)</span></span></code></pre>
<p>Eep. MySQL has re-used the ID 3. This is because the way that auto-increment
works in InnoDB is, on server restart, it will figure out what the next
ID to use is by effectively running this query:</p>
<pre class="giallo z-code" ><code data-lang="sql"><span class="giallo-l"><span class="z-13">SELECT</span><span class="z-1"> MAX</span><span>(ID) </span><span class="z-13">FROM</span><span> my_table;</span></span></code></pre>
<p>If you had deleted the most recent records from the table just before restart,
IDs that had been used will be re-used when the server comes back up.</p>
<p>In theory, this <em>shouldn't</em> cause you trouble. Best practice dictates that
you shouldn't be using IDs from database tables outside of that table unless
it's some foreign key field, and you certainly wouldn't leak that ID out of
your system, right?</p>
<p>In practice, this stuff happens and can cause devastatingly subtle bugs. MySQL
8.0 changed this behaviour by storing the auto-increment value on disk in a way
that persists across restarts.</p>
<h2 id="postgres-sequence-values-don-t-get-replicated"><a class="anchor" href="#postgres-sequence-values-don-t-get-replicated" aria-label="Link to this section">#</a>
Postgres sequence values don't get replicated</h2>
<p>Like MySQL 8.0, Postgres stores auto-increment values on disk. It does this in
a schema object called a "sequence." When you create an auto-incrementing
field in Postgres, behind the scenes a sequence will be created to back that
field and durably keep track of what the next value should be.</p>
<p>Let's take a look at that in practice.</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>$ docker volume create postgres-14-data</span></span>
<span class="giallo-l"><span>$ docker run --network host -e POSTGRES_PASSWORD=my-secret-pw -v postgres-14-data:/var/lib/postgresql/data -p postgres:14</span></span></code></pre>
<p>With Postgres up and running, let's go ahead and create our table:</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>$ docker run -it --rm --network host postgres:14 psql -h 127.0.0.1 -U postgres</span></span>
<span class="giallo-l"><span>postgres=# CREATE TABLE my_table (id SERIAL PRIMARY KEY);</span></span>
<span class="giallo-l"><span>CREATE TABLE</span></span></code></pre>
<p>And insert a few rows:</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>postgres=# INSERT INTO my_table DEFAULT VALUES;</span></span>
<span class="giallo-l"><span>INSERT 0 1</span></span>
<span class="giallo-l"><span>postgres=# INSERT INTO my_table DEFAULT VALUES;</span></span>
<span class="giallo-l"><span>INSERT 0 1</span></span>
<span class="giallo-l"><span>postgres=# INSERT INTO my_table DEFAULT VALUES;</span></span>
<span class="giallo-l"><span>INSERT 0 1</span></span>
<span class="giallo-l"><span>postgres=# SELECT * FROM my_table;</span></span>
<span class="giallo-l"><span> id</span></span>
<span class="giallo-l"><span>----</span></span>
<span class="giallo-l"><span> 1</span></span>
<span class="giallo-l"><span> 2</span></span>
<span class="giallo-l"><span> 3</span></span>
<span class="giallo-l"><span>(3 rows)</span></span></code></pre>
<p>So far so good. Let's take a look at the table:</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>postgres=# \d my_table</span></span>
<span class="giallo-l"><span> Table &quot;public.my_table&quot;</span></span>
<span class="giallo-l"><span> Column | Type | Collation | Nullable | Default</span></span>
<span class="giallo-l"><span>--------+---------+-----------+----------+--------------------------------------</span></span>
<span class="giallo-l"><span> id | integer | | not null | nextval(&#39;my_table_id_seq&#39;::regclass)</span></span>
<span class="giallo-l"><span>Indexes:</span></span>
<span class="giallo-l"><span> &quot;my_table_pkey&quot; PRIMARY KEY, btree (id)</span></span></code></pre>
<p>This output tells us that the default value for our <code>id</code> field is the <code>nextval</code>
of <code>my_table_id_seq</code>. Let's take a look at <code>my_table_id_seq</code>:</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>postgres=# \d my_table_id_seq</span></span>
<span class="giallo-l"><span> Sequence &quot;public.my_table_id_seq&quot;</span></span>
<span class="giallo-l"><span> Type | Start | Minimum | Maximum | Increment | Cycles? | Cache</span></span>
<span class="giallo-l"><span>---------+-------+---------+------------+-----------+---------+-------</span></span>
<span class="giallo-l"><span> integer | 1 | 1 | 2147483647 | 1 | no | 1</span></span>
<span class="giallo-l"><span>Owned by: public.my_table.id</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>postgres=# SELECT currval(&#39;my_table_id_seq&#39;);</span></span>
<span class="giallo-l"><span> currval</span></span>
<span class="giallo-l"><span>---------</span></span>
<span class="giallo-l"><span> 3</span></span>
<span class="giallo-l"><span>(1 row)</span></span></code></pre>
<p>Neat, we have a bonafide object in Postgres that's keeping track of the
auto-incrementing ID value. If we were to repeat what we did in MySQL, delete
some rows and restart, we wouldn't have the same problem here. <code>my_table_id_seq</code>
is saved to disk and doesn't lose its place.</p>
<p>Or does it?</p>
<p>If you want to update Postgres to a new major version, the way you typically
accomplish that is by creating a new Postgres instance on the version you want
to upgrade to, logically replicate from the old instance to the new one, and
then switch your application to talk to the new one.</p>
<p>First we need to restart our Postgres 14 with some new configuration to allow
logical replication:</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>$ docker run --network host -e POSTGRES_PASSWORD=my-secret-pw -v postgres-14-data:/var/lib/postgresql/data -p postgres:14 -c wal_level=logical</span></span></code></pre>
<p>Now let's get Postgres 15 up and running:</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>$ docker volume create postgres-15-data</span></span>
<span class="giallo-l"><span>$ docker run --network host -e POSTGRES_PASSWORD=my-secret-pw -v postgres-15-data:/var/lib/postgresql/data postgres:15 postgres:14 -c wal_level=logical -p 5431</span></span></code></pre>
<p>Next up, we create a "publication" on our Postgres 14 instance:</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>postgres=# CREATE PUBLICATION my_publication FOR ALL TABLES;</span></span>
<span class="giallo-l"><span>CREATE PUBLICATION</span></span></code></pre>
<p>Then we create our "my_table" table and a "subscription" on our Postgres 15
instance:</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>postgres=# CREATE TABLE my_table (id SERIAL PRIMARY KEY);</span></span>
<span class="giallo-l"><span>CREATE TABLE</span></span>
<span class="giallo-l"><span>postgres=# CREATE SUBSCRIPTION my_subscription CONNECTION &#39;host=127.0.0.1 port=5432 dbname=postgres user=postgres password=my-secret-pw&#39; PUBLICATION my_publication;</span></span>
<span class="giallo-l"><span>NOTICE: created replication slot &quot;my_subscription&quot; on publisher</span></span>
<span class="giallo-l"><span>CREATE SUBSCRIPTION</span></span></code></pre>
<p>After doing this, we should see data syncing between old and new instances:</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5432 -c &quot;SELECT * FROM my_table&quot;</span></span>
<span class="giallo-l"><span> id</span></span>
<span class="giallo-l"><span>----</span></span>
<span class="giallo-l"><span> 1</span></span>
<span class="giallo-l"><span> 2</span></span>
<span class="giallo-l"><span> 3</span></span>
<span class="giallo-l"><span>(3 rows)</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5431 -c &quot;SELECT * FROM my_table&quot;</span></span>
<span class="giallo-l"><span> id</span></span>
<span class="giallo-l"><span>----</span></span>
<span class="giallo-l"><span> 1</span></span>
<span class="giallo-l"><span> 2</span></span>
<span class="giallo-l"><span> 3</span></span>
<span class="giallo-l"><span>(3 rows)</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5432 -c &quot;INSERT INTO my_table DEFAULT VALUES&quot;</span></span>
<span class="giallo-l"><span>INSERT 0 1</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5431 -c &quot;SELECT * FROM my_table&quot;</span></span>
<span class="giallo-l"><span> id</span></span>
<span class="giallo-l"><span>----</span></span>
<span class="giallo-l"><span> 1</span></span>
<span class="giallo-l"><span> 2</span></span>
<span class="giallo-l"><span> 3</span></span>
<span class="giallo-l"><span> 4</span></span>
<span class="giallo-l"><span>(4 rows)</span></span></code></pre>
<p>So what's the problem?</p>
<p>Well...</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5432 -c &quot;SELECT nextval(&#39;my_table_id_seq&#39;)&quot;</span></span>
<span class="giallo-l"><span> nextval</span></span>
<span class="giallo-l"><span>---------</span></span>
<span class="giallo-l"><span> 5</span></span>
<span class="giallo-l"><span>(1 row)</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span>$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5431 -c &quot;SELECT nextval(&#39;my_table_id_seq&#39;)&quot;</span></span>
<span class="giallo-l"><span> nextval</span></span>
<span class="giallo-l"><span>---------</span></span>
<span class="giallo-l"><span> 1</span></span>
<span class="giallo-l"><span>(1 row)</span></span></code></pre>
<p>The sequence value is not replicated. If we tried to insert a row into Postgres
15 we get this:</p>
<pre class="giallo z-code" ><code data-lang="plain"><span class="giallo-l"><span>$ docker run -it --rm --network host postgres:15 psql -h 127.0.0.1 -U postgres -p 5431 -c &quot;INSERT INTO my_table DEFAULT VALUES&quot;</span></span>
<span class="giallo-l"><span>ERROR: duplicate key value violates unique constraint &quot;my_table_pkey&quot;</span></span>
<span class="giallo-l"><span>DETAIL: Key (id)=(2) already exists.</span></span></code></pre>
<p>Note: It's tried to insert id=2 here because when we called <code>nextval</code> earlier, it
modified the sequence.</p>
<p>This can make major Postgres version updates very tricky if you rely heavily on
auto-incrementing ID fields. You need to modify the sequence values manually to
values you know for a fact won't be reached during the process of the upgrade,
and then you likely need to disable writes during the upgrade depending on
your workload.</p>
<h2 id="conclusion"><a class="anchor" href="#conclusion" aria-label="Link to this section">#</a>
Conclusion</h2>
<p>You can avoid all of the above pain by using UUID fields instead of
auto-incrementing integers. These have the benefit of being unpredictable and
not leak information about the cardinality of the underlying table if you do end
up using them outside of the table (which you shouldn't).</p>
<p>Thanks to <a rel="external" href="https://incident.io/blog/one-two-skip-a-few">this article</a> from the
wonderful folks at <a rel="external" href="https://incident.io">Incident.io</a>, I am now aware of the <a rel="external" href="https://en.wikipedia.org/wiki/German_tank_problem">German tank problem</a>.
Well worth reading both the linked article, and the Wikipedia page, for more
reasons not to use auto-increment ID fields.</p>
</div>
</article>
</main>
</div>
<footer class="newsletter-footer">
<div>
Made with ❤️ by Sam Rose
</div>
<svg
id="sam-rose-signature"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
viewBox="50 154 640.1 157"
style="color: var(--color); width: 150px; display: block"
>
<style>
#sam-rose-signature path {
fill: none;
stroke: currentColor;
stroke-width: 6;
stroke-linecap: round;
stroke-dasharray: 1;
stroke-dashoffset: 1;
opacity: 0;
}
#sam-rose-signature.in-view path:nth-of-type(1) {
animation: draw 0.9s ease forwards;
}
#sam-rose-signature.in-view path:nth-of-type(2) {
animation: draw 1.5s ease-out forwards;
animation-delay: 0.9s;
}
#sam-rose-signature.in-view path:nth-of-type(3) {
animation: draw 0.4s ease forwards;
animation-delay: 2.4s;
}
@keyframes draw {
0% {
stroke-dashoffset: 1;
opacity: 0;
}
1% {
opacity: 1;
}
100% {
stroke-dashoffset: 0;
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
#sam-rose-signature path,
#sam-rose-signature.in-view path {
animation: none;
opacity: 1;
stroke-dashoffset: 0;
}
}
</style>
<path
pathLength="1"
d="M279 180h-6 0-4 0-4 0-4 0-4 0-4 0-4.1 0l-4 .5h0l-4 .6h0l-4 .6h0l-3.9.7h0l-4 .6h0l-3.9.8h0l-3.9.8h0l-4 .9h0l-3.9.8h0l-3.9.8h0l-4 .8h0l-3.9.8h0l-3.9.8h0q-2 .4-3.8 1h0L190 192h0l-3.7 1.5h0l-3.7 1.5h0q-1.9.8-3.5 1.9h0q-1.6 1-2.2 3h0q-.6 1.8-.4 3.8h0q.2 2 1.5 3.1h0q1.4 1.3 3.2 2h0l3.7 1.7h0q1.8.8 3.7 1.4h0l3.8 1.4h0l3.8 1.3h0l3.8 1.4h0l3.8 1.3h0l3.8 1.2h0l3.8 1.3h0q1.9.6 3.7 1.4h0l3.7 1.7h0l3.6 1.8h0l3.5 1.8h0q1.8 1 3.4 2.2h0l3.2 2.3h0q1.6 1.3 2.3 3.1h0q.8 1.9.6 3.8h0q-.1 2-1.2 3.5h0q-1.2 1.6-2.6 3h0q-1.4 1.4-3 2.5h0l-3.6 2h0l-3.6 1.6h0l-3.6 1.7h0l-3.8 1.5h0l-3.8 1.2h0l-3.8 1.3h0l-3.8 1.3h0l-3.8 1.2h0l-3.9 1.2h0l-3.8 1h0l-3.9 1.2h0l-3.8 1h0l-3.9 1.2h0l-3.9 1h0l-3.9.9h0l-4 .9h0l-3.8.9h0l-4 .9h0l-3.9.8h0l-3.9.9h0l-4 .7h0l-3.9.8h0l-4 .7h0l-3.9.8h0l-4 .6h0l-3.9.7h0l-4 .6h0l-4 .6h0l-3.9.7h0l-4 .5h0l-4 .5h0l-4 .4h0l-4 .5h0l-3.9.5h0l-4 .6h0l-4 .6h0l-4 .6h0l-3.9.4h0l-4 .4h0l-4 .6h0q-2 .3-3.8 1l-1.9.6"
></path>
<path
pathLength="1"
d="m54 219 7-6.2q7-6.3 15.8-10.2h0q8.8-4 18-7.4h0l18.2-6.6h0q9.1-3.3 18.4-6.1h0l18.6-5.6h0q9.3-2.7 18.7-5.1h0q9.4-2.5 19-4h0l19.2-3.1h0l19.1-3h0q9.6-1.6 19.3-2.3h0q9.7-.7 19.4-1h0q9.7-.4 19.4-.4h19.4q9.7 0 19.4.7h0q9.6.7 19.1 2.8h0q9.5 2 16.7 7.3h0q7.3 5.3 1 11.7h0q-6.2 6.4-15.2 10h0q-9 3.5-18.4 6.3h0l-18.5 5.7h0L289 208h0l-18.6 5.5h0q-9.4 2.7-15.1 5.1h0q-5.7 2.4 4 2.4h0q9.7 0 19.2-1.5h0q9.6-1.5 19.1-3.6h0q9.5-2 19-3.6h0q9.7-1.5 19.3-2.6h0q9.7-1 19.3-2.4h0q9.6-1.3 19.2-2.9h0q9.6-1.5 19.2-2.6h0q9.7-1 15.8-3.1h0q6-2.1-3.6-3h0q-9.6-.7-19.3-.5h0q-9.7.3-19.4 1.4h0q-9.6 1-18.2 5.2h0q-8.5 4.1-2.7 8.8h0q5.9 4.6 15.6 5.3h0q9.6.8 19.3 1h0q9.7.1 19.4-.6h0q9.7-.7 19.1-2.9h0q9.5-2.1 19-4.5h0q9.3-2.4 18.2-6.2h0q8.8-3.8 5.2-8.9h0q-3.7-5-13.3-6.4h0q-9.5-1.4-19.2-1.9h0q-9.7-.5-19.4-.5h0q-9.7 0-12.5 3.1h0q-2.8 3.2 6.9 3.5h0q9.6.4 19.4.4h97q9.8 0 14-1.2h0q4.2-1.3-5.4-2h0q-9.7-.8-19.4-.8h0-19.4 0q-9.8 0-19.4.4h0q-9.7.3-3.4 2.2h0q6.4 2 16 2.5h0l19.5 1.2h0l19.4 1.2h0q9.6.6 19.3 1.5h0q9.7 1 5.2 5.1h0q-4.4 4.1-14 5.6h0q-9.6 1.5-19.3 2.2h0l-19.4 1.4h0q-9.6.7-4.5 1.7h0q5.2 1 15 1.4h0q9.6.5 19.3.5h0l19.4.4h0l19.5.5h0l19.4.2h37.6q8.5-.1-1.2-1h0q-9.6-.8-19.4-.9h0l-19.4-.1h0H561h0q-9.7 0-9.6 1.6h0q.2 1.6 9.8 2.8h0q9.6 1.2 19.3 1.8h0q9.7.6 19.3 1.8h0l19.3 2.3h0l19.3 2.1h0l19.3 2.4h0q9.6 1.3 19 3.3l9.4 1.9"
></path>
<path
pathLength="1"
d="M132 307l2-1.7q2.2-1.6 4.7-2.1h0q2.6-.5 5.2-.7h0l5.4-.5h0l5.3-.4h0l5.3-.5h0l5.3-.6h0l5.4-.5h0l5.3-.8h0l5.2-.8h0l5.3-.8h0l5.3-.8h0l5.3-.9h0l5.3-.8h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.8h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.7h0l5.3-.8h0l5.3-.7h0l5.3-.7h0l5.3-.6h0l5.3-.7h0l5.3-.6h0l5.3-.6h0l5.3-.6h0l5.3-.6h0l5.3-.6h0l5.3-.6h0l5.3-.6h0l5.4-.6h0l5.3-.6h0l5.3-.5h0l5.3-.4h0l5.4-.4h0l5.3-.4h0l5.3-.4h0l5.4-.3h0l5.3-.4h0l5.3-.4h0l5.4-.4h0l5.3-.4h0l5.3-.3h0l5.4-.1h0l5.3-.1h0l5.4-.1h0l5.3-.1h0l5.4-.1h5.3l5.3-.2h5.4l5.3-.2h5.4l5.3-.1h42.8q2.7 0 5.3.2h0l5.4.2h0l5.3.3h0l5.4.3h0l5.3.2h0l5.3.3h0l5.4.3h0l5.3.3h0l5.3.5h0l5.4.5h0l5.3.5h0l5.3.5h0l5.3.6h0q2.7.2 5.3.6h0l5.3.7h0l5.3.7h0l5.3.7h0l5.3.8h0l5.3.8h0l5.3 1h0q2.6.4 5 1.2l2.5.8"
></path>
</svg>
<nav class="social-links" aria-label="Sam Rose on social media">
<a
href="https://twitter.com/samwhoo"
target="_blank"
aria-label="Follow Sam Rose on X (opens in a new tab)"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<g clip-path="url(#clip0_260_538)">
<path
d="M23.2245 5.78963C22.4373 6.13838 21.591 6.37087 20.6981 6.48075C21.606 5.94187 22.3042 5.08238 22.632 4.06688C21.7826 4.56488 20.8417 4.9305 19.8405 5.12325C19.041 4.27575 17.9006 3.75 16.6372 3.75C14.2102 3.75 12.2433 5.7 12.2433 8.10413C12.2433 8.44425 12.2831 8.77575 12.3581 9.0975C8.70708 8.91412 5.46895 7.17863 3.30258 4.54463C2.92195 5.18813 2.70783 5.94187 2.70783 6.73725C2.70783 8.24887 3.48183 9.57937 4.66195 10.3635C3.94158 10.3414 3.26358 10.1411 2.66995 9.81975C2.66995 9.83288 2.66995 9.85238 2.66995 9.87075C2.66995 11.9831 4.18495 13.7419 6.19308 14.1424C5.82595 14.2429 5.4382 14.2987 5.03845 14.2987C4.75458 14.2987 4.4782 14.2665 4.21045 14.2185C4.76958 15.9446 6.39183 17.2065 8.3137 17.2436C6.80995 18.4095 4.9162 19.1077 2.8567 19.1077C2.50083 19.1077 2.1532 19.0875 1.80933 19.0459C3.75445 20.2778 6.06483 21 8.5447 21C16.6256 21 21.0461 14.364 21.0461 8.60737C21.0461 8.41875 21.0401 8.23088 21.0311 8.04525C21.894 7.43625 22.6372 6.6675 23.2245 5.78963Z"
fill="#1DA1F2"
/>
</g>
<defs>
<clipPath id="clip0_260_538">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
</a>
<a
href="https://bsky.app/profile/samwho.dev"
target="_blank"
>
<img
src="/images/bluesky.svg"
alt="Follow Sam Rose on Bluesky (opens in a new tab)"
width="20"
height="20"
style="transform: translateY(2px)"
/>
</a>
<a href="/rss.xml">
<img src="/images/rss.svg" alt="RSS feed" width="24" height="24" />
</a>
</nav>
<p style="font-size: 0.8rem">
If you want, get updates about new posts by email. No spam, ever.
</p>
<form
action="https://buttondown.email/api/emails/embed-subscribe/samwho"
method="post"
class="embeddable-buttondown-form"
>
<label class="visually-hidden" for="bd-email">Email address</label>
<input
type="email"
name="email"
id="bd-email"
autocomplete="email"
placeholder="your@email.address"
/>
<input type="submit" value="Subscribe" />
<p>
<a href="https://buttondown.email/refer/samwho" target="_blank"
>powered by buttondown</a
>
</p>
</form>
</footer>
<script>
const signature = document.getElementById('sam-rose-signature');
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && entry.intersectionRatio === 1) {
signature.classList.add('in-view');
observer.unobserve(signature);
}
},
{
threshold: 1.0
}
);
observer.observe(signature);
</script>
</body>