fediglam/src/main/migrations.zig

195 lines
5.7 KiB
Zig

const std = @import("std");
const sql = @import("sql");
const DateTime = @import("util").DateTime;
pub const Migration = struct {
name: [:0]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(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.begin();
errdefer tx.rollback();
var remaining = script;
while (firstIndexOf(remaining, ';')) |last| {
try execStmt(tx, remaining[0 .. last + 1], alloc);
remaining = remaining[last + 1 ..];
}
if (remaining.len > 1) try execStmt(tx, remaining, alloc);
try tx.commit();
}
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 was_ran = try wasMigrationRan(db, migration.name, gpa.allocator());
if (!was_ran) {
std.log.info("Running migration {s}", .{migration.name});
try execScript(db, migration.up, gpa.allocator());
try db.insert("migration", .{ .name = migration.name }, gpa.allocator());
}
}
}
const create_migration_table =
\\CREATE TABLE IF NOT EXISTS
\\migration(
\\ name TEXT NOT NULL PRIMARY KEY,
\\ applied_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
\\);
;
// 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",
.up =
\\CREATE TABLE account(
\\ id UUID NOT NULL PRIMARY KEY,
\\ username TEXT NOT NULL,
\\
\\ kind TEXT NOT NULL CHECK (kind IN ('admin', 'user')),
\\ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
\\);
\\
\\CREATE TABLE local_account(
\\ account_id UUID NOT NULL PRIMARY KEY REFERENCES account(id),
\\
\\ email TEXT
\\);
\\
\\CREATE TABLE password(
\\ account_id UUID NOT NULL PRIMARY KEY REFERENCES account(id),
\\
\\ hash BLOB NOT NULL
\\);
,
.down =
\\DROP TABLE password;
\\DROP TABLE local_account;
\\DROP TABLE account;
,
},
.{
.name = "notes",
.up =
\\CREATE TABLE note(
\\ id UUID NOT NULL,
\\
\\ content TEXT NOT NULL,
\\ author_id UUID NOT NULL REFERENCES account(id),
\\
\\ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
\\);
,
.down = "DROP TABLE note;",
},
.{
.name = "note reactions",
.up =
\\CREATE TABLE reaction(
\\ id UUID NOT NULL PRIMARY KEY,
\\
\\ account_id UUID NOT NULL REFERENCES account(id),
\\ note_id UUID NOT NULL REFERENCES note(id),
\\
\\ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
\\);
,
.down = "DROP TABLE reaction;",
},
.{
.name = "account tokens",
.up =
\\CREATE TABLE token(
\\ hash TEXT NOT NULL PRIMARY KEY,
\\ account_id UUID NOT NULL REFERENCES local_account(id),
\\
\\ issued_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
\\);
,
.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 local_account(id),
\\
\\ max_uses INTEGER,
\\
\\ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
\\ expires_at TIMESTAMPTZ,
\\
\\ kind TEXT NOT NULL CHECK (kind in ('system_user', 'community_owner', 'user'))
\\);
\\ALTER TABLE local_account ADD COLUMN invite_id UUID REFERENCES invite(id);
,
.down =
\\ALTER TABLE local_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 DEFAULT CURRENT_TIMESTAMP
\\);
\\ALTER TABLE account 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 account DROP COLUMN community_id;
\\DROP TABLE community;
,
},
};