195 lines
5.7 KiB
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;
|
|
,
|
|
},
|
|
};
|