392 lines
13 KiB
PL/PgSQL
392 lines
13 KiB
PL/PgSQL
-- 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(); |