176 lines
4.6 KiB
Zig
176 lines
4.6 KiB
Zig
|
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;
|
||
|
,
|
||
|
},
|
||
|
};
|