Track DB migrations

This commit is contained in:
jaina heartles 2022-07-30 00:26:35 -07:00
parent 40a2391942
commit c1f8b8f0e2
2 changed files with 177 additions and 78 deletions

View file

@ -1,6 +1,7 @@
const std = @import("std");
const sql = @import("sql");
const models = @import("./db/models.zig");
const migrations = @import("./db/migrations.zig");
const util = @import("util");
const Uuid = util.Uuid;
@ -151,88 +152,11 @@ fn getAlloc(row: sql.Row, comptime T: type, idx: u15, alloc: std.mem.Allocator)
pub const Database = struct {
db: sql.Sqlite,
const init_sql_stmts = [_][]const u8{
\\CREATE TABLE IF NOT EXISTS
\\user(
\\ id TEXT NOT NULL PRIMARY KEY,
\\ username TEXT NOT NULL,
\\
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT;
,
\\CREATE TABLE IF NOT EXISTS
\\actor(
\\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
\\ public_id TEXT NOT NULL
\\) STRICT;
,
\\CREATE TABLE IF NOT EXISTS
\\local_user(
\\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
\\
\\ email TEXT,
\\
\\ hashed_password TEXT NOT NULL,
\\ password_changed_at INTEGER NOT NULL
\\) STRICT;
,
\\CREATE TABLE IF NOT EXISTS
\\note(
\\ id TEXT NOT NULL,
\\
\\ content TEXT NOT NULL,
\\ author_id TEXT NOT NULL REFERENCES actor(id),
\\
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT;
,
\\CREATE TABLE IF NOT EXISTS
\\reaction(
\\ id TEXT NOT NULL PRIMARY KEY,
\\
\\ reactor_id TEXT NOT NULL REFERENCES actor(id),
\\ note_id TEXT NOT NULL REFERENCES note(id),
\\
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT;
,
\\CREATE TABLE IF NOT EXISTS
\\token(
\\ id TEXT NOT NULL PRIMARY KEY,
\\
\\ hash BLOB UNIQUE NOT NULL,
\\ user_id TEXT NOT NULL REFERENCES local_user(id),
\\
\\ issued_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT;
,
\\CREATE TABLE IF NOT EXISTS
\\invite(
\\ id TEXT NOT NULL PRIMARY KEY,
\\
\\ name TEXT NOT NULL,
\\ invite_code TEXT NOT NULL UNIQUE,
\\ created_by TEXT NOT NULL REFERENCES local_user(id),
\\
\\ max_uses INTEGER,
\\
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
\\ expires_at INTEGER
\\) STRICT;
,
\\ALTER TABLE local_user ADD COLUMN invite_id TEXT REFERENCES invite(id);
};
pub fn init() !Database {
var db = try sql.Sqlite.open("./test.db");
errdefer db.close();
for (init_sql_stmts) |stmt_sql| {
var stmt = try db.prepare(stmt_sql);
defer stmt.finalize();
while (try stmt.step()) |_| {}
}
try migrations.up(&db);
return Database{ .db = db };
}

175
src/main/db/migrations.zig Normal file
View file

@ -0,0 +1,175 @@
const sql = @import("sql");
const DateTime = @import("util").DateTime;
pub const Migration = struct {
name: []const u8,
up: []const u8,
down: []const u8,
};
fn firstIndexOf(str: []const u8, char: u8) ?usize {
for (str) |ch, i| {
if (ch == char) return i;
}
return null;
}
fn execStmt(db: *sql.Sqlite, stmt_sql: []const u8) !void {
const stmt = try db.prepare(stmt_sql);
defer stmt.finalize();
while (try stmt.step()) |_| {}
}
fn execScript(db: *sql.Sqlite, script: []const u8) !void {
try execStmt(db, "BEGIN;");
errdefer {
_ = execStmt(db, "ROLLBACK;") catch unreachable;
}
var remaining = script;
while (firstIndexOf(remaining, ';')) |last| {
try execStmt(db, remaining[0 .. last + 1]);
remaining = remaining[last + 1 ..];
}
try execStmt(db, "COMMIT;");
}
fn wasMigrationRan(db: *sql.Sqlite, name: []const u8) !bool {
const stmt = try db.prepare("SELECT COUNT(*) FROM migration WHERE name = ?;");
defer stmt.finalize();
try stmt.bindText(1, name);
const result = (try stmt.step()).?;
const count = try result.getI64(0);
return count != 0;
}
fn markMigrationAsRan(db: *sql.Sqlite, name: []const u8) !void {
const stmt = try db.prepare("INSERT INTO migration(name) VALUES(?);");
defer stmt.finalize();
try stmt.bindText(1, name);
_ = try stmt.step();
}
pub fn up(db: *sql.Sqlite) !void {
try execScript(db, create_migration_table);
for (migrations) |migration| {
if (!try wasMigrationRan(db, migration.name)) {
try execScript(db, migration.up);
try markMigrationAsRan(db, migration.name);
}
}
}
const create_migration_table =
\\CREATE TABLE IF NOT EXISTS
\\migration(
\\ name TEXT NOT NULL PRIMARY KEY,
\\ applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT;
;
// NOTE: Until the first public release, i may collapse multiple
// migrations into a single one. this will require db recreation
const migrations: []const Migration = &.{
.{
.name = "users and actors",
.up =
\\CREATE TABLE user(
\\ id TEXT NOT NULL PRIMARY KEY,
\\ username TEXT NOT NULL,
\\
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT;
\\
\\CREATE TABLE actor(
\\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
\\ public_id TEXT NOT NULL
\\) STRICT;
\\
\\CREATE TABLE local_user(
\\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
\\
\\ email TEXT,
\\
\\ hashed_password TEXT NOT NULL,
\\ password_changed_at INTEGER NOT NULL
\\) STRICT;
,
.down =
\\DROP TABLE local_user;
\\DROP TABLE actor;
\\DROP TABLE user;
,
},
.{
.name = "notes",
.up =
\\CREATE TABLE note(
\\ id TEXT NOT NULL,
\\
\\ content TEXT NOT NULL,
\\ author_id TEXT NOT NULL REFERENCES actor(id),
\\
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT;
,
.down = "DROP TABLE note;",
},
.{
.name = "note reactions",
.up =
\\CREATE TABLE reaction(
\\ id TEXT NOT NULL PRIMARY KEY,
\\
\\ reactor_id TEXT NOT NULL REFERENCES actor(id),
\\ note_id TEXT NOT NULL REFERENCES note(id),
\\
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT;
,
.down = "DROP TABLE reaction;",
},
.{
.name = "user tokens",
.up =
\\CREATE TABLE token(
\\ id TEXT NOT NULL PRIMARY KEY,
\\
\\ hash BLOB UNIQUE NOT NULL,
\\ user_id TEXT NOT NULL REFERENCES local_user(id),
\\
\\ issued_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
\\) STRICT;
,
.down = "DROP TABLE token;",
},
.{
.name = "user invites",
.up =
\\CREATE TABLE invite(
\\ id TEXT NOT NULL PRIMARY KEY,
\\
\\ name TEXT NOT NULL,
\\ invite_code TEXT NOT NULL UNIQUE,
\\ created_by TEXT NOT NULL REFERENCES local_user(id),
\\
\\ max_uses INTEGER,
\\
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
\\ expires_at INTEGER
\\) STRICT;
\\ALTER TABLE local_user ADD COLUMN invite_id TEXT REFERENCES invite(id);
,
.down =
\\ALTER TABLE local_user DROP COLUMN invite_id;
\\DROP TABLE invite;
,
},
};