your daily cup of tea™

powered by

suckless SQLite migrations

It happened again. I wrote something that started with a small CREATE TABLE IF NOT EXISTS statement, that kept growing while manually executing DROP statements and changing it as needed. At some point I knew I would need some kind of migration system.

I’ve been there before. I searched “sql migration tool”. The results where the same as ever, a mixed bag of either declarative schema migration tools, alembic and yet more tools that are alpha, beta and carry an ORM by their hand. And that’s fine, I guess.

It happens that having been there before you know that sunk cost applies to the tool you end up going with. And then you go: how hard really is this, why not build my own. But then again, I remember all the features migration tools give me: going back between migrations, cli, etc. But then again, I also remember the troubles glueing together these tools with test suites, how most of the times I end up writing plain SQL, and so on and so forth.

This time I got lucky and found this blog post that gave me a pass for rolling my own, using PRAGMA user_version to keep track of the current schema.

And this time, I decided to go with it. This is my version, and I will go with this until it breaks and stops doing its thing. It is alpha. But at least, it’s my alpha.

First, there’s a folder with migrations.

$ ls migrations/
0001_init.sql
0002_add_foo.sql

Each migration starts setting PRAGMA user_version to a number.

$ head -n 1 migrations/0001_init.sql
PRAGMA user_version=1;
$ head -n 1 migrations/0002_add_foo.sql
PRAGMA user_version=2;

And then, there’s this I run somewhere.

import os
import sys
import sqlite3
import logging

log = logging.getlogger("foo")

DB_URI = os.getenv("DB_URI", "foo.db")
conn = sqlite3.connect(db_uri)

current_version,  = next(
    conn.cursor().execute('PRAGMA user_version'),
    (None, )
)

migrations = [
    f for f in resources.files('migrations').iterdir()
]

for migration in migrations[current_version:]:
    cur = conn.cursor()
    try:
        log.info("Applying %s", migration.name)
        cur.executescript("begin;" + migration.read_text())
    except Exception as e:
        log.error("Failed migration %s: %s. Bye", migration.name, e)
        cur.execute("rollback")
        sys.exit(1)
    else:
        cur.execute("commit")

And then I am done. Happy that I do not need to spend more time deciding between tools and if ORMs are my thing or not.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.