CREATE TABLE IF NOT EXISTS assessor_repository_state ( singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton), revision bigint NOT NULL DEFAULT 0 CHECK (revision >= 0) ); INSERT INTO assessor_repository_state (singleton, revision) VALUES (true, 0) ON CONFLICT (singleton) DO NOTHING; CREATE TABLE assessor_projects ( id text PRIMARY KEY CHECK (btrim(id) <> ''), name text NOT NULL CHECK (btrim(name) <> ''), created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, attributes jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(attributes) = 'object'), data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object') ); CREATE TABLE assessor_assessments ( id text PRIMARY KEY CHECK (btrim(id) <> ''), project_id text NOT NULL REFERENCES assessor_projects(id) ON DELETE CASCADE, name text NOT NULL CHECK (btrim(name) <> ''), created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object'), UNIQUE (id, project_id) ); CREATE TABLE assessor_scan_runs ( id text PRIMARY KEY CHECK (btrim(id) <> ''), assessment_id text NOT NULL REFERENCES assessor_assessments(id) ON DELETE CASCADE, sequence integer NOT NULL CHECK (sequence >= 0), imported_at timestamptz NOT NULL, data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object'), UNIQUE (assessment_id, sequence) ); CREATE TABLE assessor_import_files ( id text PRIMARY KEY CHECK (btrim(id) <> ''), scan_run_id text NOT NULL REFERENCES assessor_scan_runs(id) ON DELETE CASCADE, file_name text NOT NULL CHECK (btrim(file_name) <> ''), format text NOT NULL CHECK (btrim(format) <> ''), imported_at timestamptz NOT NULL, data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object') ); CREATE TABLE assessor_source_tables ( id text PRIMARY KEY CHECK (btrim(id) <> ''), import_file_id text NOT NULL REFERENCES assessor_import_files(id) ON DELETE CASCADE, table_index integer NOT NULL CHECK (table_index >= 0), name text NOT NULL, data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object'), UNIQUE (import_file_id, table_index) ); CREATE TABLE assessor_finding_definitions ( id text PRIMARY KEY CHECK (btrim(id) <> ''), canonical_key text NOT NULL CHECK (btrim(canonical_key) <> ''), data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object'), search_document tsvector GENERATED ALWAYS AS ( to_tsvector( 'simple'::regconfig, coalesce(data->>'canonicalKey', '') || ' ' || coalesce(data->>'title', '') || ' ' || coalesce(data->>'description', '') || ' ' || coalesce(data->>'category', '') || ' ' || coalesce(data->>'recommendation', '') ) ) STORED ); CREATE UNIQUE INDEX assessor_finding_definitions_canonical_key_uq ON assessor_finding_definitions (canonical_key); CREATE INDEX assessor_finding_definitions_search_idx ON assessor_finding_definitions USING gin (search_document); CREATE TABLE assessor_finding_occurrences ( id text PRIMARY KEY CHECK (btrim(id) <> ''), definition_id text NOT NULL REFERENCES assessor_finding_definitions(id) ON DELETE RESTRICT, scan_run_id text NOT NULL REFERENCES assessor_scan_runs(id) ON DELETE CASCADE, import_file_id text NOT NULL REFERENCES assessor_import_files(id) ON DELETE CASCADE, source_table_id text REFERENCES assessor_source_tables(id) ON DELETE SET NULL, tool_id text NOT NULL, severity text, finding_status text, data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object'), search_document tsvector GENERATED ALWAYS AS ( to_tsvector( 'simple'::regconfig, coalesce(data #>> '{sourceIdentity,originalRuleId}', '') || ' ' || coalesce(data #>> '{sourceIdentity,canonicalRuleId}', '') || ' ' || coalesce(data #>> '{sourceIdentity,originalTitle}', '') || ' ' || coalesce(data #>> '{affectedAsset,normalizedValue}', '') || ' ' || data::text ) ) STORED ); CREATE INDEX assessor_occurrences_scan_run_idx ON assessor_finding_occurrences (scan_run_id); CREATE INDEX assessor_occurrences_definition_idx ON assessor_finding_occurrences (definition_id); CREATE INDEX assessor_occurrences_filter_idx ON assessor_finding_occurrences (tool_id, severity, finding_status); CREATE INDEX assessor_occurrences_search_idx ON assessor_finding_occurrences USING gin (search_document); CREATE INDEX assessor_occurrences_data_idx ON assessor_finding_occurrences USING gin (data jsonb_path_ops); CREATE TABLE assessor_evidence ( id text PRIMARY KEY CHECK (btrim(id) <> ''), occurrence_id text NOT NULL REFERENCES assessor_finding_occurrences(id) ON DELETE CASCADE, data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object') ); CREATE TABLE assessor_import_diagnostics ( id text PRIMARY KEY CHECK (btrim(id) <> ''), import_file_id text NOT NULL REFERENCES assessor_import_files(id) ON DELETE CASCADE, source_table_id text REFERENCES assessor_source_tables(id) ON DELETE SET NULL, severity text NOT NULL, code text NOT NULL, data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object') ); CREATE TABLE assessor_reviews ( id text PRIMARY KEY CHECK (btrim(id) <> ''), target_kind text NOT NULL CHECK (target_kind IN ('definition', 'occurrence')), definition_id text REFERENCES assessor_finding_definitions(id) ON DELETE CASCADE, occurrence_id text REFERENCES assessor_finding_occurrences(id) ON DELETE CASCADE, revision integer NOT NULL CHECK (revision >= 1), updated_at timestamptz NOT NULL, data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object'), CHECK ( (target_kind = 'definition' AND definition_id IS NOT NULL AND occurrence_id IS NULL) OR (target_kind = 'occurrence' AND occurrence_id IS NOT NULL AND definition_id IS NULL) ) ); CREATE INDEX assessor_reviews_definition_idx ON assessor_reviews (definition_id) WHERE definition_id IS NOT NULL; CREATE INDEX assessor_reviews_occurrence_idx ON assessor_reviews (occurrence_id) WHERE occurrence_id IS NOT NULL; CREATE TABLE assessor_profile_registrations ( profile_id text NOT NULL CHECK (btrim(profile_id) <> ''), profile_version text NOT NULL CHECK (btrim(profile_version) <> ''), schema_version integer NOT NULL CHECK (schema_version >= 1), data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object'), PRIMARY KEY (profile_id, profile_version) ); CREATE TABLE assessor_local_comparison_rules ( project_id text NOT NULL REFERENCES assessor_projects(id) ON DELETE CASCADE, id text NOT NULL CHECK (btrim(id) <> ''), data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object'), PRIMARY KEY (project_id, id) ); CREATE TABLE assessor_comparisons ( id text PRIMARY KEY CHECK (btrim(id) <> ''), project_id text NOT NULL REFERENCES assessor_projects(id) ON DELETE CASCADE, assessment_id text NOT NULL, data jsonb NOT NULL CHECK (jsonb_typeof(data) = 'object'), FOREIGN KEY (assessment_id, project_id) REFERENCES assessor_assessments(id, project_id) ON DELETE CASCADE ); CREATE INDEX assessor_assessments_project_idx ON assessor_assessments (project_id); CREATE INDEX assessor_scan_runs_assessment_idx ON assessor_scan_runs (assessment_id); CREATE INDEX assessor_import_files_scan_run_idx ON assessor_import_files (scan_run_id); CREATE INDEX assessor_source_tables_file_idx ON assessor_source_tables (import_file_id); CREATE INDEX assessor_evidence_occurrence_idx ON assessor_evidence (occurrence_id); CREATE INDEX assessor_diagnostics_file_idx ON assessor_import_diagnostics (import_file_id); CREATE INDEX assessor_comparisons_assessment_idx ON assessor_comparisons (assessment_id);