-- Generated from docs/db/SPECIFICATION.md ยง5.4 (2025-11-28) CREATE SCHEMA IF NOT EXISTS scheduler; CREATE TABLE IF NOT EXISTS scheduler.schedules ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, name TEXT NOT NULL, description TEXT, enabled BOOLEAN NOT NULL DEFAULT TRUE, cron_expression TEXT, timezone TEXT NOT NULL DEFAULT 'UTC', mode TEXT NOT NULL CHECK (mode IN ('scheduled', 'manual', 'on_event', 'continuous')), selection JSONB NOT NULL DEFAULT '{}', only_if JSONB DEFAULT '{}', notify JSONB DEFAULT '{}', limits JSONB DEFAULT '{}', subscribers TEXT[] DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by TEXT, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by TEXT, deleted_at TIMESTAMPTZ, deleted_by TEXT, UNIQUE (tenant_id, name) WHERE deleted_at IS NULL ); CREATE TABLE IF NOT EXISTS scheduler.triggers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), schedule_id UUID NOT NULL REFERENCES scheduler.schedules(id) ON DELETE CASCADE, trigger_type TEXT NOT NULL CHECK (trigger_type IN ('cron', 'fixed_delay', 'manual', 'on_event', 'webhook')), cron_expression TEXT, fixed_delay_seconds INT, event_filter JSONB, timezone TEXT DEFAULT 'UTC', next_fire_time TIMESTAMPTZ, last_fire_time TIMESTAMPTZ, misfire_policy TEXT DEFAULT 'skip' CHECK (misfire_policy IN ('skip', 'fire_now', 'queue')), enabled BOOLEAN NOT NULL DEFAULT TRUE ); CREATE TABLE IF NOT EXISTS scheduler.runs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, schedule_id UUID REFERENCES scheduler.schedules(id), trigger_id UUID REFERENCES scheduler.triggers(id), state TEXT NOT NULL CHECK (state IN ('pending', 'queued', 'running', 'completed', 'failed', 'cancelled', 'stale', 'timeout')), reason JSONB DEFAULT '{}', stats JSONB DEFAULT '{}', deltas JSONB DEFAULT '[]', worker_id UUID, retry_of UUID REFERENCES scheduler.runs(id), retry_count INT NOT NULL DEFAULT 0, error TEXT, error_details JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ, timeout_at TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS scheduler.graph_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, sbom_id TEXT NOT NULL, sbom_version_id TEXT, sbom_digest TEXT NOT NULL, graph_snapshot_id TEXT, status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'completed', 'failed', 'cancelled')), trigger TEXT NOT NULL CHECK (trigger IN ('manual', 'scheduled', 'on_sbom_change', 'on_feed_update')), priority INT NOT NULL DEFAULT 100, attempts INT NOT NULL DEFAULT 0, max_attempts INT NOT NULL DEFAULT 3, cartographer_job_id TEXT, correlation_id TEXT, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, error TEXT, error_details JSONB ); CREATE TABLE IF NOT EXISTS scheduler.policy_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, policy_pack_id TEXT NOT NULL, policy_version INT, target_type TEXT NOT NULL CHECK (target_type IN ('image', 'sbom', 'project', 'artifact')), target_id TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'completed', 'failed')), priority INT NOT NULL DEFAULT 100, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, result JSONB DEFAULT '{}', error TEXT ); CREATE TABLE IF NOT EXISTS scheduler.impact_snapshots ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, run_id UUID NOT NULL REFERENCES scheduler.runs(id), image_digest TEXT NOT NULL, image_reference TEXT, new_findings INT NOT NULL DEFAULT 0, new_criticals INT NOT NULL DEFAULT 0, new_high INT NOT NULL DEFAULT 0, new_medium INT NOT NULL DEFAULT 0, new_low INT NOT NULL DEFAULT 0, total_findings INT NOT NULL DEFAULT 0, kev_hits TEXT[] DEFAULT '{}', top_findings JSONB DEFAULT '[]', report_url TEXT, attestation JSONB DEFAULT '{}', detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS scheduler.workers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), node_id TEXT NOT NULL UNIQUE, hostname TEXT, capabilities TEXT[] DEFAULT '{}', max_concurrent_jobs INT NOT NULL DEFAULT 1, current_jobs INT NOT NULL DEFAULT 0, version TEXT, last_heartbeat_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'draining', 'paused', 'dead')) ); CREATE TABLE IF NOT EXISTS scheduler.execution_logs ( id BIGSERIAL PRIMARY KEY, run_id UUID NOT NULL REFERENCES scheduler.runs(id) ON DELETE CASCADE, logged_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), level TEXT NOT NULL CHECK (level IN ('trace', 'debug', 'info', 'warn', 'error', 'fatal')), message TEXT NOT NULL, logger TEXT, data JSONB DEFAULT '{}' ); CREATE TABLE IF NOT EXISTS scheduler.locks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), lock_key TEXT NOT NULL UNIQUE, lock_type TEXT NOT NULL DEFAULT 'exclusive' CHECK (lock_type IN ('exclusive', 'shared')), holder_id TEXT NOT NULL, holder_info JSONB DEFAULT '{}', acquired_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL, renewed_at TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS scheduler.run_summaries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, schedule_id UUID REFERENCES scheduler.schedules(id), period_start TIMESTAMPTZ NOT NULL, period_end TIMESTAMPTZ NOT NULL, total_runs INT NOT NULL DEFAULT 0, successful_runs INT NOT NULL DEFAULT 0, failed_runs INT NOT NULL DEFAULT 0, cancelled_runs INT NOT NULL DEFAULT 0, avg_duration_seconds NUMERIC(10,2), max_duration_seconds INT, min_duration_seconds INT, total_findings_detected INT NOT NULL DEFAULT 0, new_criticals INT NOT NULL DEFAULT 0, computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (tenant_id, schedule_id, period_start) ); CREATE TABLE IF NOT EXISTS scheduler.audit ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, action TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id UUID NOT NULL, actor TEXT, actor_type TEXT CHECK (actor_type IN ('user', 'service', 'system')), old_value JSONB, new_value JSONB, details JSONB DEFAULT '{}', ip_address INET, occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Indexes CREATE INDEX IF NOT EXISTS idx_schedules_tenant ON scheduler.schedules(tenant_id) WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_schedules_enabled ON scheduler.schedules(tenant_id, enabled) WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_triggers_schedule ON scheduler.triggers(schedule_id); CREATE INDEX IF NOT EXISTS idx_triggers_next_fire ON scheduler.triggers(next_fire_time) WHERE enabled = TRUE; CREATE INDEX IF NOT EXISTS idx_runs_tenant_state ON scheduler.runs(tenant_id, state); CREATE INDEX IF NOT EXISTS idx_runs_schedule ON scheduler.runs(schedule_id); CREATE INDEX IF NOT EXISTS idx_runs_created ON scheduler.runs(created_at DESC); CREATE INDEX IF NOT EXISTS idx_runs_state_created ON scheduler.runs(state, created_at) WHERE state IN ('pending', 'queued', 'running'); CREATE INDEX IF NOT EXISTS idx_graph_jobs_tenant_status ON scheduler.graph_jobs(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_graph_jobs_sbom ON scheduler.graph_jobs(sbom_digest); CREATE INDEX IF NOT EXISTS idx_policy_jobs_tenant_status ON scheduler.policy_jobs(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_impact_snapshots_run ON scheduler.impact_snapshots(run_id); CREATE INDEX IF NOT EXISTS idx_impact_snapshots_tenant ON scheduler.impact_snapshots(tenant_id, detected_at DESC); CREATE INDEX IF NOT EXISTS idx_workers_status ON scheduler.workers(status); CREATE INDEX IF NOT EXISTS idx_workers_heartbeat ON scheduler.workers(last_heartbeat_at); CREATE INDEX IF NOT EXISTS idx_execution_logs_run ON scheduler.execution_logs(run_id); CREATE INDEX IF NOT EXISTS idx_locks_expires ON scheduler.locks(expires_at); CREATE INDEX IF NOT EXISTS idx_run_summaries_tenant ON scheduler.run_summaries(tenant_id, period_start DESC); CREATE INDEX IF NOT EXISTS idx_audit_tenant_time ON scheduler.audit(tenant_id, occurred_at DESC); CREATE INDEX IF NOT EXISTS idx_audit_entity ON scheduler.audit(entity_type, entity_id); -- ============================================================================= -- Failure Signatures table for predictive TTFS signal hints -- Tracks common failure patterns by scope, toolchain, and error code -- Added: Sprint 0341 -- ============================================================================= CREATE TABLE IF NOT EXISTS scheduler.failure_signatures ( signature_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Scope: what artifact/repo/image this signature applies to scope_type TEXT NOT NULL CHECK (scope_type IN ('repo', 'image', 'artifact', 'global')), scope_id TEXT NOT NULL, -- Toolchain: build environment fingerprint toolchain_hash TEXT NOT NULL, -- Error classification error_code TEXT NULL, error_category TEXT NULL CHECK (error_category IN ('network', 'auth', 'validation', 'resource', 'timeout', 'config', 'unknown')), -- Signature statistics occurrence_count INT NOT NULL DEFAULT 1, first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Resolution status resolution_status TEXT NOT NULL DEFAULT 'unresolved' CHECK (resolution_status IN ('unresolved', 'investigating', 'resolved', 'wont_fix')), resolution_notes TEXT NULL, resolved_at TIMESTAMPTZ NULL, resolved_by TEXT NULL, -- Predictive hints predicted_outcome TEXT NULL CHECK (predicted_outcome IN ('pass', 'fail', 'flaky', 'unknown')), confidence_score DECIMAL(5, 4) NULL CHECK (confidence_score >= 0 AND confidence_score <= 1), -- Composite unique constraint UNIQUE (tenant_id, scope_type, scope_id, toolchain_hash, error_code) ); -- Indexes for failure_signatures CREATE INDEX IF NOT EXISTS idx_failure_sig_tenant ON scheduler.failure_signatures(tenant_id); CREATE INDEX IF NOT EXISTS idx_failure_sig_scope ON scheduler.failure_signatures(scope_type, scope_id); CREATE INDEX IF NOT EXISTS idx_failure_sig_error ON scheduler.failure_signatures(error_code) WHERE error_code IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_failure_sig_last_seen ON scheduler.failure_signatures(last_seen_at DESC); CREATE INDEX IF NOT EXISTS idx_failure_sig_unresolved ON scheduler.failure_signatures(tenant_id, resolution_status) WHERE resolution_status = 'unresolved';