Files
periodic-table/db/periodic-table.sql
gulimabr 384375a297 edit sql
2026-01-18 22:55:50 -03:00

482 lines
15 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();
-- ACCEPTANCE_CRITERIA: Individual conditions a requirement must meet
CREATE TABLE acceptance_criteria (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
requirement_id INT NOT NULL,
criteria_text TEXT NOT NULL,
is_accepted BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_ac_requirement FOREIGN KEY (requirement_id) REFERENCES requirements (id) ON DELETE CASCADE
);
-- Index for performance when loading a requirement's page
CREATE INDEX idx_ac_requirement ON acceptance_criteria(requirement_id);
CREATE OR REPLACE FUNCTION update_modified_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_ac_modtime
BEFORE UPDATE ON acceptance_criteria
FOR EACH ROW
EXECUTE FUNCTION update_modified_column();
-- Add editor tracking to the main table
ALTER TABLE acceptance_criteria
ADD COLUMN last_editor_id INT;
ALTER TABLE acceptance_criteria
ADD CONSTRAINT fk_ac_editor FOREIGN KEY (last_editor_id) REFERENCES users (id);
-- ACCEPTANCE_CRITERIA_HISTORY: Tracks changes to criteria and acceptance state
CREATE TABLE acceptance_criteria_history (
history_id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
original_ac_id INT NOT NULL,
requirement_id INT,
criteria_text TEXT,
is_accepted BOOLEAN,
valid_from TIMESTAMPTZ,
valid_to TIMESTAMPTZ DEFAULT NOW(),
edited_by INT,
CONSTRAINT fk_ach_user FOREIGN KEY (edited_by) REFERENCES users (id)
);
-- Index for auditing specific requirements
CREATE INDEX idx_ach_req ON acceptance_criteria_history(requirement_id);
-- Function to archive the state of an acceptance criterion
CREATE OR REPLACE FUNCTION archive_acceptance_criteria_change()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO acceptance_criteria_history (
original_ac_id,
requirement_id,
criteria_text,
is_accepted,
valid_from,
valid_to,
edited_by
)
VALUES (
OLD.id,
OLD.requirement_id,
OLD.criteria_text,
OLD.is_accepted,
OLD.updated_at, -- When this version started
NOW(), -- When this version was replaced/deleted
OLD.last_editor_id
);
IF (TG_OP = 'DELETE') THEN
RETURN OLD;
ELSE
NEW.updated_at := NOW(); -- Update the timestamp on the current record
RETURN NEW;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Bind the trigger to the table
CREATE TRIGGER trigger_audit_acceptance_criteria
BEFORE UPDATE OR DELETE ON acceptance_criteria
FOR EACH ROW
EXECUTE FUNCTION archive_acceptance_criteria_change();