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 std = @import("std");
|
||||||
const sql = @import("sql");
|
const sql = @import("sql");
|
||||||
const models = @import("./db/models.zig");
|
const models = @import("./db/models.zig");
|
||||||
|
const migrations = @import("./db/migrations.zig");
|
||||||
const util = @import("util");
|
const util = @import("util");
|
||||||
|
|
||||||
const Uuid = util.Uuid;
|
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 {
|
pub const Database = struct {
|
||||||
db: sql.Sqlite,
|
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 {
|
pub fn init() !Database {
|
||||||
var db = try sql.Sqlite.open("./test.db");
|
var db = try sql.Sqlite.open("./test.db");
|
||||||
errdefer db.close();
|
errdefer db.close();
|
||||||
|
|
||||||
for (init_sql_stmts) |stmt_sql| {
|
try migrations.up(&db);
|
||||||
var stmt = try db.prepare(stmt_sql);
|
|
||||||
defer stmt.finalize();
|
|
||||||
|
|
||||||
while (try stmt.step()) |_| {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Database{ .db = 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