fediglam/src/main/db/migrations.zig

176 lines
4.6 KiB
Zig
Raw Normal View History

2022-07-30 07:26:35 +00:00
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;
,
},
};