-- TAGS: Global (Shared across all projects) CREATE TABLE tags ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, tag_code VARCHAR(10) NOT NULL UNIQUE, tag_description TEXT NOT NULL ); -- GROUPS: Global (Shared across all projects) CREATE TABLE groups ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, group_name TEXT NOT NULL UNIQUE, hex_color VARCHAR(7) NOT NULL ); -- PRIORITIES: Global CREATE TABLE priorities ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, priority_name TEXT NOT NULL UNIQUE, priority_num INT NOT NULL DEFAULT 0 ); -- VALIDATION STATUSES: Global (Approved, Rejected, etc.) CREATE TABLE validation_statuses ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, status_name TEXT NOT NULL UNIQUE ); -- [NEW] REQUIREMENT STATUSES: Global (Draft, Regular, Deprecated) -- This allows you to manage lifecycle states dynamically. CREATE TABLE requirement_statuses ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, status_code VARCHAR(20) NOT NULL UNIQUE, -- e.g. 'DRAFT' status_name TEXT NOT NULL, -- e.g. 'Draft' description TEXT ); -- Seed initial statuses so the FKs work immediately INSERT INTO requirement_statuses (status_code, status_name, description) VALUES ('DRAFT', 'Draft', 'Initial version, not ready for review'), ('REGULAR', 'Regular', 'Active requirement'); -- ROLES: System-wide roles (e.g., Admin, User) CREATE TABLE roles ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, role_name TEXT NOT NULL UNIQUE ); -- USERS: System-wide users CREATE TABLE users ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, sub TEXT NOT NULL UNIQUE, -- Keycloak Subject ID username TEXT NOT NULL, full_name TEXT, role_id INT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT fk_users_role FOREIGN KEY (role_id) REFERENCES roles (id) ); -- PROJECTS: The container for requirements CREATE TABLE projects ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, project_name TEXT NOT NULL, project_desc TEXT, created_at TIMESTAMPTZ DEFAULT NOW() ); -- PROJECT_MEMBERS: Controls Access (Many-to-Many) CREATE TABLE project_members ( project_id INT NOT NULL, user_id INT NOT NULL, joined_at TIMESTAMPTZ DEFAULT NOW(), PRIMARY KEY (project_id, user_id), CONSTRAINT fk_pm_project FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, CONSTRAINT fk_pm_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); -- REQUIREMENTS: Updated with Status ID CREATE TABLE requirements ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, project_id INT NOT NULL, user_id INT NOT NULL, tag_id INT NOT NULL, status_id INT NOT NULL DEFAULT 1, -- [NEW] Defaults to ID 1 (Draft) last_editor_id INT, req_name TEXT NOT NULL, req_desc TEXT, priority_id INT, version INT NOT NULL DEFAULT 1, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT fk_req_project FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, CONSTRAINT fk_req_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL, CONSTRAINT fk_req_editor FOREIGN KEY (last_editor_id) REFERENCES users (id), CONSTRAINT fk_req_priority FOREIGN KEY (priority_id) REFERENCES priorities (id), CONSTRAINT fk_req_tag FOREIGN KEY (tag_id) REFERENCES tags (id), CONSTRAINT fk_req_status FOREIGN KEY (status_id) REFERENCES requirement_statuses (id) -- [NEW] ); -- REQUIREMENTS_GROUPS: Join table for M:N relationship CREATE TABLE requirements_groups ( requirement_id INT NOT NULL, group_id INT NOT NULL, PRIMARY KEY (requirement_id, group_id), CONSTRAINT fk_rg_req FOREIGN KEY (requirement_id) REFERENCES requirements (id) ON DELETE CASCADE, CONSTRAINT fk_rg_group FOREIGN KEY (group_id) REFERENCES groups (id) ON DELETE CASCADE ); -- VALIDATIONS: No change CREATE TABLE validations ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, requirement_id INT NOT NULL, user_id INT NOT NULL, status_id INT NOT NULL, req_version_snapshot INT NOT NULL, comment TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT fk_validation_req FOREIGN KEY (requirement_id) REFERENCES requirements (id) ON DELETE CASCADE, CONSTRAINT fk_validation_user FOREIGN KEY (user_id) REFERENCES users (id), CONSTRAINT fk_validation_status FOREIGN KEY (status_id) REFERENCES validation_statuses (id) ); -- REQUIREMENTS_HISTORY: Updated to track 'status_id' changes CREATE TABLE requirements_history ( history_id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, original_req_id INT NOT NULL, project_id INT, status_id INT, -- [NEW] Added to history req_name TEXT, req_desc TEXT, priority_id INT, tag_id INT, version INT, valid_from TIMESTAMPTZ, valid_to TIMESTAMPTZ, edited_by INT ); -- The function to archive the old row before update CREATE OR REPLACE FUNCTION archive_requirement_change() RETURNS TRIGGER AS $$ BEGIN INSERT INTO requirements_history ( original_req_id, project_id, status_id, -- [NEW] req_name, req_desc, priority_id, tag_id, version, valid_from, valid_to, edited_by ) VALUES ( OLD.id, OLD.project_id, OLD.status_id, -- [NEW] OLD.req_name, OLD.req_desc, OLD.priority_id, OLD.tag_id, OLD.version, OLD.updated_at, NOW(), OLD.last_editor_id ); NEW.version := OLD.version + 1; NEW.updated_at := NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- Bind Trigger to requirements CREATE TRIGGER trigger_audit_requirements BEFORE UPDATE ON requirements FOR EACH ROW EXECUTE FUNCTION archive_requirement_change(); -- Indexes for performance CREATE INDEX idx_req_project ON requirements(project_id); CREATE INDEX idx_req_tag ON requirements(tag_id); CREATE INDEX idx_req_priority ON requirements(priority_id); CREATE INDEX idx_req_user ON requirements(user_id); CREATE INDEX idx_req_status ON requirements(status_id); -- [NEW] CREATE INDEX idx_pm_user ON project_members(user_id); -- RELATIONSHIP_TYPES: Defines valid connection types per project CREATE TABLE relationship_types ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, project_id INT NOT NULL, type_name TEXT NOT NULL, type_description TEXT, inverse_type_name TEXT, CONSTRAINT fk_rel_type_project FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, CONSTRAINT uq_rel_type_name_project UNIQUE (project_id, type_name) ); -- REQUIREMENT_LINKS: The actual connections between requirements CREATE TABLE requirement_links ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, source_req_id INT NOT NULL, target_req_id INT NOT NULL, relationship_type_id INT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), created_by INT, CONSTRAINT fk_link_source FOREIGN KEY (source_req_id) REFERENCES requirements (id) ON DELETE CASCADE, CONSTRAINT fk_link_target FOREIGN KEY (target_req_id) REFERENCES requirements (id) ON DELETE CASCADE, CONSTRAINT fk_link_type FOREIGN KEY (relationship_type_id) REFERENCES relationship_types (id) ON DELETE CASCADE, CONSTRAINT fk_link_creator FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL, CONSTRAINT check_no_self_link CHECK (source_req_id <> target_req_id), CONSTRAINT uq_req_link_pair UNIQUE (source_req_id, target_req_id, relationship_type_id) ); CREATE INDEX idx_link_source ON requirement_links(source_req_id); CREATE INDEX idx_link_target ON requirement_links(target_req_id); -- REQUIREMENT_COMMENTS: Top-level comments CREATE TABLE requirement_comments ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, requirement_id INT NOT NULL, user_id INT, comment_text TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), is_deleted BOOLEAN DEFAULT FALSE, CONSTRAINT fk_rc_req FOREIGN KEY (requirement_id) REFERENCES requirements (id) ON DELETE CASCADE, CONSTRAINT fk_rc_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL ); -- REQUIREMENT_COMMENT_REPLIES: Responses to top-level comments CREATE TABLE requirement_comment_replies ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, parent_comment_id INT NOT NULL, user_id INT, reply_text TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), is_deleted BOOLEAN DEFAULT FALSE, CONSTRAINT fk_rcr_parent FOREIGN KEY (parent_comment_id) REFERENCES requirement_comments (id) ON DELETE CASCADE, CONSTRAINT fk_rcr_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL ); CREATE INDEX idx_rc_req ON requirement_comments(requirement_id); CREATE INDEX idx_rcr_parent ON requirement_comment_replies(parent_comment_id); -- 1. HISTORY TABLE: Captures deleted/changed links with Snapshots CREATE TABLE requirement_links_history ( history_id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, original_link_id INT, -- The ID of the link from the main table source_req_id INT, target_req_id INT, relationship_type_id INT, -- Kept for reference relationship_type_snapshot TEXT, -- [IMPORTANT] Text copy of type name (e.g. "Depends On") inverse_type_snapshot TEXT, -- [IMPORTANT] Text copy of inverse name (e.g. "Is Depended On By") created_by INT, valid_from TIMESTAMPTZ, -- When the link was created valid_to TIMESTAMPTZ DEFAULT NOW() -- When the link was deleted/changed ); -- Indexes for querying link history CREATE INDEX idx_link_hist_source ON requirement_links_history(source_req_id); CREATE INDEX idx_link_hist_target ON requirement_links_history(target_req_id); -- 2. FUNCTION: Handles the archiving with Snapshot lookup CREATE OR REPLACE FUNCTION archive_link_change() RETURNS TRIGGER AS $$ DECLARE v_type_name TEXT; v_inverse_name TEXT; BEGIN -- Fetch current names from the configuration table to snapshot them SELECT type_name, inverse_type_name INTO v_type_name, v_inverse_name FROM relationship_types WHERE id = OLD.relationship_type_id; -- Insert into history with the text snapshots INSERT INTO requirement_links_history ( original_link_id, source_req_id, target_req_id, relationship_type_id, relationship_type_snapshot, inverse_type_snapshot, created_by, valid_from, valid_to ) VALUES ( OLD.id, OLD.source_req_id, OLD.target_req_id, OLD.relationship_type_id, v_type_name, -- Saved permanently v_inverse_name, -- Saved permanently OLD.created_by, OLD.created_at, NOW() ); IF (TG_OP = 'DELETE') THEN RETURN OLD; ELSE RETURN NEW; END IF; END; $$ LANGUAGE plpgsql; -- 3. TRIGGER: Binds the function to the table CREATE TRIGGER trigger_audit_links BEFORE UPDATE OR DELETE ON requirement_links FOR EACH ROW EXECUTE FUNCTION archive_link_change(); -- 1. PRE-REQUISITE: Add created_at to the join table -- We need this to know when the relationship began (valid_from). ALTER TABLE requirements_groups ADD COLUMN created_at TIMESTAMPTZ DEFAULT NOW(); -- 2. HISTORY TABLE: Captures removed/changed group associations CREATE TABLE requirements_groups_history ( history_id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, requirement_id INT NOT NULL, group_id INT NOT NULL, group_name_snapshot TEXT, group_hex_color_snapshot VARCHAR(7), valid_from TIMESTAMPTZ, -- When the association was created valid_to TIMESTAMPTZ DEFAULT NOW() -- When the association was removed/changed ); -- Index for querying history by requirement CREATE INDEX idx_req_groups_hist_req ON requirements_groups_history(requirement_id); -- 3. FUNCTION: Handles the archiving with Snapshot lookup CREATE OR REPLACE FUNCTION archive_requirements_groups_change() RETURNS TRIGGER AS $$ DECLARE v_group_name TEXT; v_hex_color VARCHAR(7); BEGIN -- Fetch current group details to snapshot them -- We do this so the history remains readable even if the Group ID is deleted later SELECT group_name, hex_color INTO v_group_name, v_hex_color FROM groups WHERE id = OLD.group_id; -- Insert into history INSERT INTO requirements_groups_history ( requirement_id, group_id, group_name_snapshot, group_hex_color_snapshot, valid_from, valid_to ) VALUES ( OLD.requirement_id, OLD.group_id, v_group_name, -- Saved permanently v_hex_color, -- Saved permanently OLD.created_at, -- The time it was originally linked NOW() -- The time it was removed ); IF (TG_OP = 'DELETE') THEN RETURN OLD; ELSE RETURN NEW; END IF; END; $$ LANGUAGE plpgsql; -- 4. TRIGGER: Binds the function to the table -- Triggers on DELETE (removing a group) or UPDATE (swapping a group ID directly) CREATE TRIGGER trigger_audit_requirements_groups BEFORE UPDATE OR DELETE ON requirements_groups FOR EACH ROW EXECUTE FUNCTION archive_requirements_groups_change();