-- Migration 011: Resource event tables, entity references, and dependent fetch queue -- Powers temporal queries (timeline, file-history, trace) via GitLab Resource Events APIs. -- State change events (opened/closed/reopened/merged/locked) CREATE TABLE resource_state_events ( id INTEGER PRIMARY KEY, gitlab_id INTEGER NOT NULL, project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE, issue_id INTEGER REFERENCES issues(id) ON DELETE CASCADE, merge_request_id INTEGER REFERENCES merge_requests(id) ON DELETE CASCADE, state TEXT NOT NULL, actor_gitlab_id INTEGER, actor_username TEXT, created_at INTEGER NOT NULL, -- ms epoch UTC source_commit TEXT, source_merge_request_iid INTEGER, -- iid from source_merge_request ref CHECK ( (issue_id IS NOT NULL AND merge_request_id IS NULL) OR (issue_id IS NULL AND merge_request_id IS NOT NULL) ) ); CREATE UNIQUE INDEX uq_state_events_gitlab ON resource_state_events(gitlab_id, project_id); CREATE INDEX idx_state_events_issue ON resource_state_events(issue_id) WHERE issue_id IS NOT NULL; CREATE INDEX idx_state_events_mr ON resource_state_events(merge_request_id) WHERE merge_request_id IS NOT NULL; CREATE INDEX idx_state_events_created ON resource_state_events(created_at); -- Label change events (add/remove) CREATE TABLE resource_label_events ( id INTEGER PRIMARY KEY, gitlab_id INTEGER NOT NULL, project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE, issue_id INTEGER REFERENCES issues(id) ON DELETE CASCADE, merge_request_id INTEGER REFERENCES merge_requests(id) ON DELETE CASCADE, action TEXT NOT NULL CHECK (action IN ('add', 'remove')), label_name TEXT NOT NULL, actor_gitlab_id INTEGER, actor_username TEXT, created_at INTEGER NOT NULL, -- ms epoch UTC CHECK ( (issue_id IS NOT NULL AND merge_request_id IS NULL) OR (issue_id IS NULL AND merge_request_id IS NOT NULL) ) ); CREATE UNIQUE INDEX uq_label_events_gitlab ON resource_label_events(gitlab_id, project_id); CREATE INDEX idx_label_events_issue ON resource_label_events(issue_id) WHERE issue_id IS NOT NULL; CREATE INDEX idx_label_events_mr ON resource_label_events(merge_request_id) WHERE merge_request_id IS NOT NULL; CREATE INDEX idx_label_events_created ON resource_label_events(created_at); -- Milestone change events (add/remove) CREATE TABLE resource_milestone_events ( id INTEGER PRIMARY KEY, gitlab_id INTEGER NOT NULL, project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE, issue_id INTEGER REFERENCES issues(id) ON DELETE CASCADE, merge_request_id INTEGER REFERENCES merge_requests(id) ON DELETE CASCADE, action TEXT NOT NULL CHECK (action IN ('add', 'remove')), milestone_title TEXT NOT NULL, milestone_id INTEGER, actor_gitlab_id INTEGER, actor_username TEXT, created_at INTEGER NOT NULL, -- ms epoch UTC CHECK ( (issue_id IS NOT NULL AND merge_request_id IS NULL) OR (issue_id IS NULL AND merge_request_id IS NOT NULL) ) ); CREATE UNIQUE INDEX uq_milestone_events_gitlab ON resource_milestone_events(gitlab_id, project_id); CREATE INDEX idx_milestone_events_issue ON resource_milestone_events(issue_id) WHERE issue_id IS NOT NULL; CREATE INDEX idx_milestone_events_mr ON resource_milestone_events(merge_request_id) WHERE merge_request_id IS NOT NULL; CREATE INDEX idx_milestone_events_created ON resource_milestone_events(created_at); -- Cross-reference table (Gate 2): source/target entity pairs CREATE TABLE entity_references ( id INTEGER PRIMARY KEY, project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE, source_entity_type TEXT NOT NULL CHECK (source_entity_type IN ('issue', 'merge_request')), source_entity_id INTEGER NOT NULL, -- local DB id target_entity_type TEXT NOT NULL CHECK (target_entity_type IN ('issue', 'merge_request')), target_entity_id INTEGER, -- local DB id (NULL if unresolved) target_project_path TEXT, -- for unresolved cross-project refs target_entity_iid INTEGER, -- for unresolved refs reference_type TEXT NOT NULL CHECK (reference_type IN ('closes', 'mentioned', 'related')), source_method TEXT NOT NULL CHECK (source_method IN ('api', 'note_parse', 'description_parse')), created_at INTEGER NOT NULL -- ms epoch UTC ); CREATE UNIQUE INDEX uq_entity_refs ON entity_references( project_id, source_entity_type, source_entity_id, target_entity_type, COALESCE(target_entity_id, -1), COALESCE(target_project_path, ''), COALESCE(target_entity_iid, -1), reference_type, source_method ); CREATE INDEX idx_entity_refs_source ON entity_references(source_entity_type, source_entity_id); CREATE INDEX idx_entity_refs_target ON entity_references(target_entity_id) WHERE target_entity_id IS NOT NULL; CREATE INDEX idx_entity_refs_unresolved ON entity_references(target_project_path, target_entity_iid) WHERE target_entity_id IS NULL; -- Generic dependent fetch queue (resource_events, mr_closes_issues, mr_diffs) CREATE TABLE pending_dependent_fetches ( id INTEGER PRIMARY KEY, project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE, entity_type TEXT NOT NULL CHECK (entity_type IN ('issue', 'merge_request')), entity_iid INTEGER NOT NULL, entity_local_id INTEGER NOT NULL, job_type TEXT NOT NULL CHECK (job_type IN ('resource_events', 'mr_closes_issues', 'mr_diffs')), payload_json TEXT, -- optional extra data for the job enqueued_at INTEGER NOT NULL, -- ms epoch UTC locked_at INTEGER, -- ms epoch UTC (NULL = available) attempts INTEGER NOT NULL DEFAULT 0, next_retry_at INTEGER, -- ms epoch UTC (NULL = no backoff) last_error TEXT ); CREATE UNIQUE INDEX uq_pending_fetches ON pending_dependent_fetches(project_id, entity_type, entity_iid, job_type); CREATE INDEX idx_pending_fetches_claimable ON pending_dependent_fetches(job_type, locked_at) WHERE locked_at IS NULL; CREATE INDEX idx_pending_fetches_retryable ON pending_dependent_fetches(next_retry_at) WHERE locked_at IS NULL AND next_retry_at IS NOT NULL; -- Update schema version INSERT INTO schema_version (version, applied_at, description) VALUES (11, strftime('%s', 'now') * 1000, 'Resource events, entity references, and dependent fetch queue');