fediglam/src/main/migrations.zig

348 lines
10 KiB
Zig
Raw Normal View History

2022-09-15 01:12:07 +00:00
const std = @import("std");
2022-07-30 07:26:35 +00:00
const sql = @import("sql");
const util = @import("util");
const DateTime = util.DateTime;
2022-07-30 07:26:35 +00:00
pub const Migration = struct {
2022-09-15 01:12:07 +00:00
name: [:0]const u8,
2022-07-30 07:26:35 +00:00
up: []const u8,
down: []const u8,
};
2022-10-04 05:41:22 +00:00
fn execStmt(tx: anytype, stmt: []const u8, alloc: std.mem.Allocator) !void {
2022-09-15 01:12:07 +00:00
const stmt_null = try std.cstr.addNullByte(alloc, stmt);
defer alloc.free(stmt_null);
2022-10-02 05:18:24 +00:00
try tx.exec(stmt_null, {}, null);
2022-07-30 07:26:35 +00:00
}
2022-10-04 05:41:22 +00:00
fn execScript(db: anytype, script: []const u8, alloc: std.mem.Allocator) !void {
2022-10-13 06:19:59 +00:00
const tx = try db.beginOrSavepoint();
errdefer tx.rollback();
2022-12-02 04:52:51 +00:00
var iter = std.mem.split(u8, script, ";");
while (iter.next()) |stmt| {
2022-12-02 04:52:51 +00:00
if (stmt.len == 0) continue;
2022-10-13 06:19:59 +00:00
try execStmt(tx, stmt, alloc);
2022-07-30 07:26:35 +00:00
}
2022-10-13 06:19:59 +00:00
try tx.commitOrRelease();
2022-07-30 07:26:35 +00:00
}
2022-09-25 08:10:30 +00:00
fn wasMigrationRan(db: anytype, name: []const u8, alloc: std.mem.Allocator) !bool {
2022-10-04 02:41:59 +00:00
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,
};
2022-07-30 07:26:35 +00:00
}
2022-10-04 05:41:22 +00:00
pub fn up(db: anytype) !void {
2022-09-15 01:12:07 +00:00
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
std.log.info("Running migrations...", .{});
try execScript(db, create_migration_table, gpa.allocator());
2022-07-30 07:26:35 +00:00
for (migrations) |migration| {
const tx = try db.begin();
errdefer tx.rollback();
const was_ran = try wasMigrationRan(tx, migration.name, gpa.allocator());
2022-09-15 01:12:07 +00:00
if (!was_ran) {
std.log.info("Running migration {s}", .{migration.name});
try execScript(tx, migration.up, gpa.allocator());
try tx.insert("migration", .{
2022-11-04 06:26:50 +00:00
.name = @as([]const u8, migration.name),
.applied_at = DateTime.now(),
}, gpa.allocator());
2022-07-30 07:26:35 +00:00
}
try tx.commit();
2022-07-30 07:26:35 +00:00
}
}
const create_migration_table =
\\CREATE TABLE IF NOT EXISTS
\\migration(
\\ name TEXT NOT NULL PRIMARY KEY,
\\ applied_at TIMESTAMPTZ NOT NULL
2022-09-05 09:15:16 +00:00
\\);
2022-07-30 07:26:35 +00:00
;
// 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 and actors",
2022-07-30 07:26:35 +00:00
.up =
\\CREATE TABLE actor(
2022-10-04 02:41:59 +00:00
\\ id UUID NOT NULL PRIMARY KEY,
2022-07-30 07:26:35 +00:00
\\ username TEXT NOT NULL,
\\
\\ created_at TIMESTAMPTZ NOT NULL
2022-09-05 08:52:49 +00:00
\\);
2022-07-30 07:26:35 +00:00
\\
\\CREATE TABLE account(
\\ id UUID NOT NULL PRIMARY KEY REFERENCES actor(id),
2022-09-07 23:14:52 +00:00
\\
\\ kind TEXT NOT NULL CHECK (kind IN ('admin', 'user')),
2022-09-07 23:14:52 +00:00
\\ email TEXT
2022-09-05 08:52:49 +00:00
\\);
2022-07-30 07:26:35 +00:00
\\
2022-10-04 02:41:59 +00:00
\\CREATE TABLE password(
\\ account_id UUID NOT NULL PRIMARY KEY REFERENCES account(id),
2022-07-30 07:26:35 +00:00
\\
\\ hash BLOB NOT NULL,
\\ changed_at TIMESTAMPTZ NOT NULL
2022-09-05 08:52:49 +00:00
\\);
2022-07-30 07:26:35 +00:00
,
.down =
2022-10-04 02:41:59 +00:00
\\DROP TABLE password;
\\DROP TABLE account;
\\DROP TABLE actor;
2022-07-30 07:26:35 +00:00
,
},
.{
.name = "notes",
.up =
\\CREATE TABLE note(
2022-10-04 02:41:59 +00:00
\\ id UUID NOT NULL,
2022-07-30 07:26:35 +00:00
\\
\\ content TEXT NOT NULL,
\\ author_id UUID NOT NULL REFERENCES actor(id),
2022-07-30 07:26:35 +00:00
\\
\\ created_at TIMESTAMPTZ NOT NULL
2022-09-07 23:14:52 +00:00
\\);
2022-07-30 07:26:35 +00:00
,
.down = "DROP TABLE note;",
},
.{
.name = "note reactions",
.up =
\\CREATE TABLE reaction(
2022-10-04 02:41:59 +00:00
\\ id UUID NOT NULL PRIMARY KEY,
2022-07-30 07:26:35 +00:00
\\
\\ author_id UUID NOT NULL REFERENCES actor(id),
2022-10-04 02:41:59 +00:00
\\ note_id UUID NOT NULL REFERENCES note(id),
2022-07-30 07:26:35 +00:00
\\
\\ created_at TIMESTAMPTZ NOT NULL
2022-09-07 23:14:52 +00:00
\\);
2022-07-30 07:26:35 +00:00
,
.down = "DROP TABLE reaction;",
},
.{
2022-10-04 02:41:59 +00:00
.name = "account tokens",
2022-07-30 07:26:35 +00:00
.up =
\\CREATE TABLE token(
2022-09-07 23:14:52 +00:00
\\ hash TEXT NOT NULL PRIMARY KEY,
\\ account_id UUID NOT NULL REFERENCES account(id),
2022-07-30 07:26:35 +00:00
\\
\\ issued_at TIMESTAMPTZ NOT NULL
2022-09-07 23:14:52 +00:00
\\);
2022-07-30 07:26:35 +00:00
,
.down = "DROP TABLE token;",
},
.{
2022-10-04 02:41:59 +00:00
.name = "account invites",
2022-07-30 07:26:35 +00:00
.up =
\\CREATE TABLE invite(
2022-10-04 02:41:59 +00:00
\\ id UUID NOT NULL PRIMARY KEY,
2022-07-30 07:26:35 +00:00
\\
\\ name TEXT NOT NULL,
2022-09-08 06:56:29 +00:00
\\ code TEXT NOT NULL UNIQUE,
\\ created_by UUID NOT NULL REFERENCES account(id),
2022-07-30 07:26:35 +00:00
\\
\\ max_uses INTEGER,
\\
\\ created_at TIMESTAMPTZ NOT NULL,
2022-09-15 01:12:07 +00:00
\\ expires_at TIMESTAMPTZ,
2022-09-08 06:56:29 +00:00
\\
2022-10-04 02:41:59 +00:00
\\ kind TEXT NOT NULL CHECK (kind in ('system_user', 'community_owner', 'user'))
2022-09-07 23:14:52 +00:00
\\);
\\ALTER TABLE account ADD COLUMN invite_id UUID REFERENCES invite(id);
2022-07-30 07:26:35 +00:00
,
.down =
\\ALTER TABLE account DROP COLUMN invite_id;
2022-07-30 07:26:35 +00:00
\\DROP TABLE invite;
,
},
2022-08-02 04:33:23 +00:00
.{
.name = "communities",
.up =
\\CREATE TABLE community(
2022-10-04 02:41:59 +00:00
\\ id UUID NOT NULL PRIMARY KEY,
2022-08-02 04:33:23 +00:00
\\
2022-10-04 02:41:59 +00:00
\\ owner_id UUID REFERENCES account(id),
2022-08-02 04:33:23 +00:00
\\ name TEXT NOT NULL,
\\ host TEXT NOT NULL UNIQUE,
\\ scheme TEXT NOT NULL CHECK (scheme IN ('http', 'https')),
2022-09-29 21:52:01 +00:00
\\ kind TEXT NOT NULL CHECK (kind in ('admin', 'local')),
2022-08-02 04:33:23 +00:00
\\
\\ created_at TIMESTAMPTZ NOT NULL
2022-09-05 09:15:16 +00:00
\\);
\\ALTER TABLE actor ADD COLUMN community_id UUID REFERENCES community(id);
2022-10-04 02:41:59 +00:00
\\ALTER TABLE invite ADD COLUMN community_id UUID REFERENCES community(id);
2022-08-02 04:33:23 +00:00
,
.down =
2022-09-29 21:52:01 +00:00
\\ALTER TABLE invite DROP COLUMN community_id;
\\ALTER TABLE actor DROP COLUMN community_id;
2022-08-02 04:33:23 +00:00
\\DROP TABLE community;
,
},
2022-11-14 07:56:58 +00:00
.{
.name = "follows",
.up =
\\CREATE TABLE follow(
\\ id UUID NOT NULL PRIMARY KEY,
\\
2022-11-15 04:25:59 +00:00
\\ followed_by_id UUID NOT NULL,
\\ followee_id UUID NOT NULL,
2022-11-14 07:56:58 +00:00
\\
\\ created_at TIMESTAMPTZ NOT NULL,
\\
2022-11-15 04:25:59 +00:00
\\ UNIQUE(followed_by_id, followee_id)
2022-11-14 07:56:58 +00:00
\\);
,
.down = "DROP TABLE follow",
},
2022-11-21 11:58:54 +00:00
.{
.name = "files",
.up =
2022-12-03 15:09:03 +00:00
\\CREATE TABLE file_upload(
2022-11-21 11:58:54 +00:00
\\ id UUID NOT NULL PRIMARY KEY,
\\
2022-12-03 15:09:03 +00:00
\\ created_by UUID REFERENCES account(id),
\\ size INTEGER NOT NULL,
\\
2022-11-21 11:58:54 +00:00
\\ filename TEXT NOT NULL,
2022-12-03 15:09:03 +00:00
\\ description TEXT,
\\ content_type TEXT,
\\ sensitive BOOLEAN NOT NULL,
\\
\\ is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
\\
\\ created_at TIMESTAMPTZ NOT NULL,
\\ updated_at TIMESTAMPTZ NOT NULL
\\);
\\
\\CREATE TABLE drive_entry(
\\ id UUID NOT NULL PRIMARY KEY,
\\
2022-11-21 11:58:54 +00:00
\\ account_owner_id UUID REFERENCES account(id),
\\ community_owner_id UUID REFERENCES community(id),
\\
2022-12-03 15:09:03 +00:00
\\ name TEXT,
\\ parent_directory_id UUID REFERENCES drive_entry(id),
\\
\\ file_id UUID REFERENCES file_upload(id),
2022-11-21 11:58:54 +00:00
\\
\\ CHECK(
\\ (account_owner_id IS NULL AND community_owner_id IS NOT NULL)
\\ OR (account_owner_id IS NOT NULL AND community_owner_id IS NULL)
2022-12-03 15:09:03 +00:00
\\ ),
\\ CHECK(
\\ (name IS NULL AND parent_directory_id IS NULL AND file_id IS NULL)
\\ OR (name IS NOT NULL AND parent_directory_id IS NOT NULL)
2022-11-21 11:58:54 +00:00
\\ )
\\);
2022-12-03 15:09:03 +00:00
\\CREATE UNIQUE INDEX drive_entry_uniqueness
\\ON drive_entry(
\\ name,
\\ COALESCE(parent_directory_id, ''),
\\ COALESCE(account_owner_id, community_owner_id)
\\);
,
.down =
\\DROP INDEX drive_entry_uniqueness;
\\DROP TABLE drive_entry;
\\DROP TABLE file_upload;
,
},
.{
.name = "drive_entry_path",
.up =
\\CREATE VIEW drive_entry_path(
\\ id,
\\ path,
\\ account_owner_id,
\\ community_owner_id,
\\ kind
\\) AS WITH RECURSIVE full_path(
\\ id,
\\ path,
\\ account_owner_id,
\\ community_owner_id,
\\ kind
\\) AS (
\\ SELECT
\\ id,
\\ '' AS path,
\\ account_owner_id,
\\ community_owner_id,
\\ 'dir' AS kind
\\ FROM drive_entry
\\ WHERE parent_directory_id IS NULL
\\ UNION ALL
\\ SELECT
\\ base.id,
\\ (dir.path || '/' || base.name) AS path,
\\ base.account_owner_id,
\\ base.community_owner_id,
\\ (CASE WHEN base.file_id IS NULL THEN 'dir' ELSE 'file' END) as kind
\\ FROM drive_entry AS base
\\ JOIN full_path AS dir ON
\\ base.parent_directory_id = dir.id
\\ AND base.account_owner_id IS NOT DISTINCT FROM dir.account_owner_id
\\ AND base.community_owner_id IS NOT DISTINCT FROM dir.community_owner_id
\\)
\\SELECT
\\ id,
\\ (CASE WHEN kind = 'dir' THEN path || '/' ELSE path END) AS path,
\\ account_owner_id,
\\ community_owner_id,
\\ kind
\\FROM full_path;
,
.down =
\\DROP VIEW drive_entry_path;
,
},
.{
.name = "create drive root directories",
.up =
\\INSERT INTO drive_entry(
\\ id,
\\ account_owner_id,
\\ community_owner_id,
\\ parent_directory_id,
\\ name,
\\ file_id
\\) SELECT
\\ id,
\\ id AS account_owner_id,
\\ NULL AS community_owner_id,
\\ NULL AS parent_directory_id,
\\ NULL AS name,
\\ NULL AS file_id
\\FROM account;
\\INSERT INTO drive_entry(
\\ id,
\\ account_owner_id,
\\ community_owner_id,
\\ parent_directory_id,
\\ name,
\\ file_id
\\) SELECT
\\ id,
\\ NULL AS account_owner_id,
\\ id AS community_owner_id,
\\ NULL AS parent_directory_id,
\\ NULL AS name,
\\ NULL AS file_id
\\FROM community;
2022-11-21 11:58:54 +00:00
,
2022-12-03 15:09:03 +00:00
.down = "",
2022-11-21 11:58:54 +00:00
},
2022-07-30 07:26:35 +00:00
};