fediglam/src/main/migrations.zig

193 lines
5.5 KiB
Zig

const std = @import("std");
const sql = @import("sql");
const util = @import("util");
const DateTime = util.DateTime;
pub const Migration = struct {
name: [:0]const u8,
up: []const u8,
down: []const u8,
};
fn execStmt(tx: anytype, stmt: []const u8, alloc: std.mem.Allocator) !void {
const stmt_null = try std.cstr.addNullByte(alloc, stmt);
defer alloc.free(stmt_null);
try tx.exec(stmt_null, {}, null);
}
fn execScript(db: anytype, script: []const u8, alloc: std.mem.Allocator) !void {
const tx = try db.beginOrSavepoint();
errdefer tx.rollback();
var iter = util.SqlStmtIter.from(script);
while (iter.next()) |stmt| {
try execStmt(tx, stmt, alloc);
}
try tx.commitOrRelease();
}
fn wasMigrationRan(db: anytype, name: []const u8, alloc: std.mem.Allocator) !bool {
return if (db.queryRow(
std.meta.Tuple(&.{i32}),
"SELECT COUNT(*) FROM migration WHERE name = $1 LIMIT 1",
.{name},
alloc,
)) |row| row[0] != 0 else |err| switch (err) {
error.NoRows => false,
else => error.DatabaseFailure,
};
}
pub fn up(db: anytype) !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
std.log.info("Running migrations...", .{});
try execScript(db, create_migration_table, gpa.allocator());
for (migrations) |migration| {
const tx = try db.begin();
errdefer tx.rollback();
const was_ran = try wasMigrationRan(tx, migration.name, gpa.allocator());
if (!was_ran) {
std.log.info("Running migration {s}", .{migration.name});
try execScript(tx, migration.up, gpa.allocator());
try tx.insert("migration", .{
.name = migration.name,
.applied_at = DateTime.now(),
}, gpa.allocator());
}
try tx.commit();
}
}
const create_migration_table =
\\CREATE TABLE IF NOT EXISTS
\\migration(
\\ name TEXT NOT NULL PRIMARY KEY,
\\ applied_at TIMESTAMPTZ NOT NULL
\\);
;
// 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 = "accounts and actors",
.up =
\\CREATE TABLE actor(
\\ id UUID NOT NULL PRIMARY KEY,
\\ username TEXT NOT NULL,
\\
\\ created_at TIMESTAMPTZ NOT NULL
\\);
\\
\\CREATE TABLE account(
\\ id UUID NOT NULL PRIMARY KEY REFERENCES actor(id),
\\
\\ kind TEXT NOT NULL CHECK (kind IN ('admin', 'user')),
\\ email TEXT
\\);
\\
\\CREATE TABLE password(
\\ account_id UUID NOT NULL PRIMARY KEY REFERENCES account(id),
\\
\\ hash BLOB NOT NULL,
\\ changed_at TIMESTAMPTZ NOT NULL
\\);
,
.down =
\\DROP TABLE password;
\\DROP TABLE account;
\\DROP TABLE actor;
,
},
.{
.name = "notes",
.up =
\\CREATE TABLE note(
\\ id UUID NOT NULL,
\\
\\ content TEXT NOT NULL,
\\ author_id UUID NOT NULL REFERENCES actor(id),
\\
\\ created_at TIMESTAMPTZ NOT NULL
\\);
,
.down = "DROP TABLE note;",
},
.{
.name = "note reactions",
.up =
\\CREATE TABLE reaction(
\\ id UUID NOT NULL PRIMARY KEY,
\\
\\ author_id UUID NOT NULL REFERENCES actor(id),
\\ note_id UUID NOT NULL REFERENCES note(id),
\\
\\ created_at TIMESTAMPTZ NOT NULL
\\);
,
.down = "DROP TABLE reaction;",
},
.{
.name = "account tokens",
.up =
\\CREATE TABLE token(
\\ hash TEXT NOT NULL PRIMARY KEY,
\\ account_id UUID NOT NULL REFERENCES account(id),
\\
\\ issued_at TIMESTAMPTZ NOT NULL
\\);
,
.down = "DROP TABLE token;",
},
.{
.name = "account invites",
.up =
\\CREATE TABLE invite(
\\ id UUID NOT NULL PRIMARY KEY,
\\
\\ name TEXT NOT NULL,
\\ code TEXT NOT NULL UNIQUE,
\\ created_by UUID NOT NULL REFERENCES account(id),
\\
\\ max_uses INTEGER,
\\
\\ created_at TIMESTAMPTZ NOT NULL,
\\ expires_at TIMESTAMPTZ,
\\
\\ kind TEXT NOT NULL CHECK (kind in ('system_user', 'community_owner', 'user'))
\\);
\\ALTER TABLE account ADD COLUMN invite_id UUID REFERENCES invite(id);
,
.down =
\\ALTER TABLE account DROP COLUMN invite_id;
\\DROP TABLE invite;
,
},
.{
.name = "communities",
.up =
\\CREATE TABLE community(
\\ id UUID NOT NULL PRIMARY KEY,
\\
\\ owner_id UUID REFERENCES account(id),
\\ name TEXT NOT NULL,
\\ host TEXT NOT NULL UNIQUE,
\\ scheme TEXT NOT NULL CHECK (scheme IN ('http', 'https')),
\\ kind TEXT NOT NULL CHECK (kind in ('admin', 'local')),
\\
\\ created_at TIMESTAMPTZ NOT NULL
\\);
\\ALTER TABLE actor ADD COLUMN community_id UUID REFERENCES community(id);
\\ALTER TABLE invite ADD COLUMN community_id UUID REFERENCES community(id);
,
.down =
\\ALTER TABLE invite DROP COLUMN community_id;
\\ALTER TABLE actor DROP COLUMN community_id;
\\DROP TABLE community;
,
},
};