From c7344a9e376a3e764fe82b35e5c59b2ef9be94bc Mon Sep 17 00:00:00 2001 From: Brandon Dyck Date: Sat, 6 Jul 2024 11:41:55 -0600 Subject: [PATCH] Run DB migrations on startup --- assets/.init.lua | 6 +- assets/.lua/migrate.lua | 100 ++++++++++++++++++----- assets/migrations/01 create node.sql | 6 ++ assets/migrations/2 create node_type.sql | 9 ++ 4 files changed, 101 insertions(+), 20 deletions(-) diff --git a/assets/.init.lua b/assets/.init.lua index 1adb2db..c0c2a32 100644 --- a/assets/.init.lua +++ b/assets/.init.lua @@ -2,7 +2,11 @@ local migrate = require "migrate" -migrate.migrate() +local DB_FILENAME = "peachy.db" +sqlite3 = require "lsqlite3" +local db = sqlite3.open(DB_FILENAME) +db:exec("PRAGMA journal_mode=WAL;") +migrate.migrate(db) fm = require "fullmoon" fm.setTemplate("hello", "Hello, {%& name %}") diff --git a/assets/.lua/migrate.lua b/assets/.lua/migrate.lua index d4985ba..ee8fd42 100644 --- a/assets/.lua/migrate.lua +++ b/assets/.lua/migrate.lua @@ -3,30 +3,92 @@ local path = require "path" local filenameRegex = re.compile([=[^([0-9]+).*\.sql$]=]) -local function migrate() - local migrations = {} - local seqnums = {} - local paths = GetZipPaths("/migrations/") - for _, p in ipairs(paths) do - -- basename -> sequence number - -- check that sequence number doesn't already exist - local basename = path.basename(p) - local _, seqnum = filenameRegex:search(basename) - seqnum = tonumber(seqnum) - if seqnum and not seqnums[seqnum] then - table.insert(seqnums, seqnum) - table.insert(migrations, { - filename = p, - seqnum = seqnum, - }) +local function throwSqlError(db, prefix) + local code = db:errcode() + if code ~= sqlite3.OK and code ~= sqlite3.ROW and code ~= sqlite3.DONE then + if prefix then + error(string.format("%s: %s", prefix, db:errmsg())) else - print(string.format("found weird migration name: %s", p)) + error(db:errmsg()) end end - +end + +local function transact(db, func, errorPrefix) + local name = string.format("savepoint_%d", math.abs(math.random(0))) + db:exec(string.format("savepoint "..name)) + throwSqlError(db, errorPrefix) + local success, result = pcall(func) + if not success then + db:exec("rollback to "..name) + error(result, errorPrefix) + end + db:exec("release "..name) + return result +end + +local function getCurrentMigration(db) + for seqnum in db:urows("select max(seqnum) from migrations") do return seqnum end + throwSqlError(db, "failed to get last migration sequence number", db) + return -1 +end + +local function recordMigration(db, seqnum) + local stmt = db:prepare("insert into migrations (seqnum) values (?)") + stmt:bind_values(seqnum) + throwSqlError(db, "failed to record migration") + stmt:step() +end + +local function migrate(db) + local migrations = {} + local seqnums = {} + local MIGRATION_PATH = "/migrations/" + local paths = GetZipPaths(MIGRATION_PATH) + for _, p in ipairs(paths) do + -- check that sequence number doesn't already exist + if p ~= MIGRATION_PATH then + local basename = path.basename(p) + local _, seqnum = filenameRegex:search(basename) + seqnum = tonumber(seqnum) + if seqnum and not seqnums[seqnum] then + table.insert(seqnums, seqnum) + table.insert(migrations, { + filename = p, + seqnum = seqnum, + }) + else + print(string.format("found weird migration name: %s", p)) + end + end + end + + db:exec[[ + create table if not exists migrations ( + seqnum integer primary key + ) without rowid + ]] + throwSqlError(db, "failed to create migrations table") + table.sort(migrations, function(a, b) return a.seqnum < b.seqnum end) + local announced for i,mig in ipairs(migrations) do - print(string.format("%d: %s", i, mig.filename)) + local seqnum = mig.seqnum + local lastSeqnum = getCurrentMigration(db) + if seqnum > lastSeqnum then + if not announced then + print"Applying migrations:" + announced = true + end + print(mig.filename) + local migsql = LoadAsset(mig.filename) + transact(db, function() + db:exec(migsql) + throwSqlError(db, "migration failed") + recordMigration(db, seqnum) + seqnum = seqnum + 1 + end) + end end end diff --git a/assets/migrations/01 create node.sql b/assets/migrations/01 create node.sql index e69de29..33fc0d8 100644 --- a/assets/migrations/01 create node.sql +++ b/assets/migrations/01 create node.sql @@ -0,0 +1,6 @@ +create table node ( + id INT PRIMARY KEY, + title TEXT NOT NULL, + body TEXT NOT NULL +); +insert into node(title, body) values ("First post", "This is the beginning of a beautiful relationship."); diff --git a/assets/migrations/2 create node_type.sql b/assets/migrations/2 create node_type.sql index e69de29..0363dd0 100644 --- a/assets/migrations/2 create node_type.sql +++ b/assets/migrations/2 create node_type.sql @@ -0,0 +1,9 @@ +create table node_type ( + id INT PRIMARY KEY, + name TEXT NOT NULL +); + +insert into node_type(id, name) values (0, "Basic post"); + +alter table node +add column node_type_id INT NOT NULL references node_type(id) default 0; \ No newline at end of file