aboutsummaryrefslogtreecommitdiff
path: root/sql/schema.sql
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--sql/schema.sql195
1 files changed, 195 insertions, 0 deletions
diff --git a/sql/schema.sql b/sql/schema.sql
new file mode 100644
index 0000000..a6efc39
--- /dev/null
+++ b/sql/schema.sql
@@ -0,0 +1,195 @@
+-- SPDX-License-Identifier: AGPL-3.0-only
+-- SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+
+CREATE TABLE groups (
+ id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ name TEXT NOT NULL,
+ parent_group INTEGER REFERENCES groups(id) ON DELETE CASCADE,
+ description TEXT,
+ UNIQUE NULLS NOT DISTINCT (parent_group, name)
+);
+
+CREATE TABLE repos (
+ id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE RESTRICT, -- I mean, should be CASCADE but deleting Git repos on disk also needs to be considered
+ contrib_requirements TEXT NOT NULL CHECK (contrib_requirements IN ('closed', 'registered_user', 'federated', 'ssh_pubkey', 'public')),
+ name TEXT NOT NULL,
+ UNIQUE(group_id, name),
+ description TEXT,
+ filesystem_path TEXT
+);
+
+CREATE TABLE mailing_lists (
+ id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE RESTRICT,
+ name TEXT NOT NULL,
+ UNIQUE(group_id, name),
+ description TEXT
+);
+
+CREATE TABLE mailing_list_emails (
+ id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ list_id INTEGER NOT NULL REFERENCES mailing_lists(id) ON DELETE CASCADE,
+ title TEXT NOT NULL,
+ sender TEXT NOT NULL,
+ date TIMESTAMP NOT NULL,
+ content BYTEA NOT NULL
+);
+
+CREATE TABLE users (
+ id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ username TEXT UNIQUE,
+ type TEXT NOT NULL CHECK (type IN ('pubkey_only', 'federated', 'registered')),
+ password TEXT
+);
+
+CREATE TABLE ssh_public_keys (
+ id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ key_string TEXT NOT NULL,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ CONSTRAINT unique_key_string EXCLUDE USING HASH (key_string WITH =)
+);
+
+CREATE TABLE sessions (
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ session_id TEXT PRIMARY KEY NOT NULL,
+ UNIQUE(user_id, session_id)
+);
+
+CREATE TABLE user_group_roles (
+ group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ PRIMARY KEY(user_id, group_id)
+);
+
+CREATE TABLE federated_identities (
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ service TEXT NOT NULL,
+ remote_username TEXT NOT NULL,
+ PRIMARY KEY(user_id, service)
+);
+
+-- Ticket tracking
+
+CREATE TABLE ticket_trackers (
+ id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE RESTRICT,
+ name TEXT NOT NULL,
+ description TEXT,
+ UNIQUE(group_id, name)
+);
+
+CREATE TABLE tickets (
+ id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ tracker_id INTEGER NOT NULL REFERENCES ticket_trackers(id) ON DELETE CASCADE,
+ tracker_local_id INTEGER NOT NULL,
+ title TEXT NOT NULL,
+ description TEXT,
+ UNIQUE(tracker_id, tracker_local_id)
+);
+
+CREATE OR REPLACE FUNCTION create_tracker_ticket_sequence()
+RETURNS TRIGGER AS $$
+DECLARE
+ seq_name TEXT := 'tracker_ticket_seq_' || NEW.id;
+BEGIN
+ EXECUTE format('CREATE SEQUENCE %I', seq_name);
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER after_insert_ticket_tracker
+AFTER INSERT ON ticket_trackers
+FOR EACH ROW
+EXECUTE FUNCTION create_tracker_ticket_sequence();
+
+CREATE OR REPLACE FUNCTION drop_tracker_ticket_sequence()
+RETURNS TRIGGER AS $$
+DECLARE
+ seq_name TEXT := 'tracker_ticket_seq_' || OLD.id;
+BEGIN
+ EXECUTE format('DROP SEQUENCE IF EXISTS %I', seq_name);
+ RETURN OLD;
+END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER before_delete_ticket_tracker
+BEFORE DELETE ON ticket_trackers
+FOR EACH ROW
+EXECUTE FUNCTION drop_tracker_ticket_sequence();
+
+CREATE OR REPLACE FUNCTION assign_tracker_local_id()
+RETURNS TRIGGER AS $$
+DECLARE
+ seq_name TEXT := 'tracker_ticket_seq_' || NEW.tracker_id;
+BEGIN
+ IF NEW.tracker_local_id IS NULL THEN
+ EXECUTE format('SELECT nextval(%L)', seq_name)
+ INTO NEW.tracker_local_id;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER before_insert_ticket
+BEFORE INSERT ON tickets
+FOR EACH ROW
+EXECUTE FUNCTION assign_tracker_local_id();
+
+-- Merge requests
+
+CREATE TABLE merge_requests (
+ id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
+ repo_local_id INTEGER NOT NULL,
+ title TEXT,
+ creator INTEGER REFERENCES users(id) ON DELETE SET NULL,
+ source_ref TEXT NOT NULL,
+ destination_branch TEXT,
+ status TEXT NOT NULL CHECK (status IN ('open', 'merged', 'closed')),
+ UNIQUE (repo_id, repo_local_id),
+ UNIQUE (repo_id, source_ref, destination_branch)
+);
+
+CREATE OR REPLACE FUNCTION create_repo_mr_sequence()
+RETURNS TRIGGER AS $$
+DECLARE
+ seq_name TEXT := 'repo_mr_seq_' || NEW.id;
+BEGIN
+ EXECUTE format('CREATE SEQUENCE %I', seq_name);
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER after_insert_repo
+AFTER INSERT ON repos
+FOR EACH ROW
+EXECUTE FUNCTION create_repo_mr_sequence();
+
+CREATE OR REPLACE FUNCTION drop_repo_mr_sequence()
+RETURNS TRIGGER AS $$
+DECLARE
+ seq_name TEXT := 'repo_mr_seq_' || OLD.id;
+BEGIN
+ EXECUTE format('DROP SEQUENCE IF EXISTS %I', seq_name);
+ RETURN OLD;
+END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER before_delete_repo
+BEFORE DELETE ON repos
+FOR EACH ROW
+EXECUTE FUNCTION drop_repo_mr_sequence();
+
+
+CREATE OR REPLACE FUNCTION assign_repo_local_id()
+RETURNS TRIGGER AS $$
+DECLARE
+ seq_name TEXT := 'repo_mr_seq_' || NEW.repo_id;
+BEGIN
+ IF NEW.repo_local_id IS NULL THEN
+ EXECUTE format('SELECT nextval(%L)', seq_name)
+ INTO NEW.repo_local_id;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER before_insert_merge_request
+BEFORE INSERT ON merge_requests
+FOR EACH ROW
+EXECUTE FUNCTION assign_repo_local_id();