Track DB migrations
This commit is contained in:
parent
40a2391942
commit
c1f8b8f0e2
2 changed files with 177 additions and 78 deletions
|
@ -1,6 +1,7 @@
|
|||
const std = @import("std");
|
||||
const sql = @import("sql");
|
||||
const models = @import("./db/models.zig");
|
||||
const migrations = @import("./db/migrations.zig");
|
||||
const util = @import("util");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
|
@ -151,88 +152,11 @@ fn getAlloc(row: sql.Row, comptime T: type, idx: u15, alloc: std.mem.Allocator)
|
|||
pub const Database = struct {
|
||||
db: sql.Sqlite,
|
||||
|
||||
const init_sql_stmts = [_][]const u8{
|
||||
\\CREATE TABLE IF NOT EXISTS
|
||||
\\user(
|
||||
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||
\\ username TEXT NOT NULL,
|
||||
\\
|
||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
\\) STRICT;
|
||||
,
|
||||
\\CREATE TABLE IF NOT EXISTS
|
||||
\\actor(
|
||||
\\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
|
||||
\\ public_id TEXT NOT NULL
|
||||
\\) STRICT;
|
||||
,
|
||||
\\CREATE TABLE IF NOT EXISTS
|
||||
\\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;
|
||||
,
|
||||
\\CREATE TABLE IF NOT EXISTS
|
||||
\\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;
|
||||
,
|
||||
\\CREATE TABLE IF NOT EXISTS
|
||||
\\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;
|
||||
,
|
||||
\\CREATE TABLE IF NOT EXISTS
|
||||
\\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;
|
||||
,
|
||||
\\CREATE TABLE IF NOT EXISTS
|
||||
\\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);
|
||||
};
|
||||
|
||||
pub fn init() !Database {
|
||||
var db = try sql.Sqlite.open("./test.db");
|
||||
errdefer db.close();
|
||||
|
||||
for (init_sql_stmts) |stmt_sql| {
|
||||
var stmt = try db.prepare(stmt_sql);
|
||||
defer stmt.finalize();
|
||||
|
||||
while (try stmt.step()) |_| {}
|
||||
}
|
||||
try migrations.up(&db);
|
||||
|
||||
return Database{ .db = db };
|
||||
}
|
||||
|
|
175
src/main/db/migrations.zig
Normal file
175
src/main/db/migrations.zig
Normal file
|
@ -0,0 +1,175 @@
|
|||
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;
|
||||
,
|
||||
},
|
||||
};
|
Loading…
Reference in a new issue