from __future__ import annotations import sqlite3 from collections.abc import Iterable from contextlib import closing from src.domain.contracts import ( AgentJobErrorCategory, AgentJobStatus, AgentJobType, ArticleWorkflowStatus, AssetStatus, AssetType, ClaimRiskLevel, ClaimSupportStatus, PlanReviewStatus, PublishingStatus, Role, ScriptConfigVersionStatus, ) def _quoted_values(values: Iterable[str]) -> str: return ", ".join(f"'{value}'" for value in values) ROLE_VALUES = _quoted_values(role.value for role in Role) ARTICLE_STATUS_VALUES = _quoted_values(status.value for status in ArticleWorkflowStatus) PUBLISHING_STATUS_VALUES = _quoted_values(status.value for status in PublishingStatus) SCRIPT_CONFIG_STATUS_VALUES = _quoted_values( status.value for status in ScriptConfigVersionStatus ) PLAN_REVIEW_STATUS_VALUES = _quoted_values(status.value for status in PlanReviewStatus) CLAIM_SUPPORT_STATUS_VALUES = _quoted_values( status.value for status in ClaimSupportStatus ) CLAIM_RISK_LEVEL_VALUES = _quoted_values(level.value for level in ClaimRiskLevel) ASSET_TYPE_VALUES = _quoted_values(asset_type.value for asset_type in AssetType) ASSET_STATUS_VALUES = _quoted_values(status.value for status in AssetStatus) AGENT_JOB_TYPE_VALUES = _quoted_values(job_type.value for job_type in AgentJobType) AGENT_JOB_STATUS_VALUES = _quoted_values(status.value for status in AgentJobStatus) AGENT_JOB_ERROR_CATEGORY_VALUES = _quoted_values( category.value for category in AgentJobErrorCategory ) POSTGRES_SCHEMA_STATEMENTS: tuple[str, ...] = ( 'CREATE EXTENSION IF NOT EXISTS "pgcrypto"', f""" CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, role TEXT NOT NULL CHECK (role IN ({ROLE_VALUES})), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, """ CREATE TABLE IF NOT EXISTS target_sites ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, publishing_type TEXT NOT NULL DEFAULT 'git_next', default_language TEXT NOT NULL DEFAULT 'en', brand_voice TEXT NOT NULL, audience TEXT NOT NULL, seo_rules JSONB NOT NULL DEFAULT '{}'::jsonb, visual_rules JSONB NOT NULL DEFAULT '{}'::jsonb, source_rules JSONB NOT NULL DEFAULT '{}'::jsonb, publishing_rules JSONB NOT NULL DEFAULT '{}'::jsonb, active_script_config_version_id UUID, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, f""" CREATE TABLE IF NOT EXISTS script_config_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), target_site_id UUID NOT NULL REFERENCES target_sites(id) ON DELETE CASCADE, version INTEGER NOT NULL, status TEXT NOT NULL CHECK (status IN ({SCRIPT_CONFIG_STATUS_VALUES})), created_by UUID NOT NULL REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), diff JSONB NOT NULL DEFAULT '{{}}'::jsonb, rollback_target_version_id UUID REFERENCES script_config_versions(id), activated_at TIMESTAMPTZ, publishing_yaml TEXT NOT NULL, publishing_yaml_hash TEXT NOT NULL, transform_script TEXT NOT NULL, transform_script_hash TEXT NOT NULL, UNIQUE (target_site_id, version) ) """, f""" CREATE TABLE IF NOT EXISTS articles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), target_site_id UUID NOT NULL REFERENCES target_sites(id), status TEXT NOT NULL CHECK (status IN ({ARTICLE_STATUS_VALUES})), publishing_status TEXT NOT NULL CHECK ( publishing_status IN ({PUBLISHING_STATUS_VALUES}) ), brief_description TEXT NOT NULL, working_title TEXT, language TEXT NOT NULL DEFAULT 'en', content_type TEXT NOT NULL DEFAULT 'longform_guide', primary_keyword TEXT, assigned_editor_id UUID REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, """ CREATE TABLE IF NOT EXISTS boundary_questions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, sort_order INTEGER NOT NULL, category TEXT NOT NULL DEFAULT 'general', question TEXT NOT NULL, answer TEXT, is_required BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (article_id, sort_order) ) """, "ALTER TABLE boundary_questions ADD COLUMN IF NOT EXISTS category TEXT NOT NULL DEFAULT 'general'", f""" CREATE TABLE IF NOT EXISTS article_plans ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, version INTEGER NOT NULL, status TEXT NOT NULL CHECK (status IN ({PLAN_REVIEW_STATUS_VALUES})), title_options JSONB NOT NULL DEFAULT '[]'::jsonb, recommended_title TEXT, reader_persona TEXT, search_intent TEXT, thesis TEXT, claims_to_prove JSONB NOT NULL DEFAULT '[]'::jsonb, evidence_needs JSONB NOT NULL DEFAULT '[]'::jsonb, visual_needs JSONB NOT NULL DEFAULT '[]'::jsonb, seo_notes JSONB NOT NULL DEFAULT '[]'::jsonb, source_requirements JSONB NOT NULL DEFAULT '[]'::jsonb, excluded_sources JSONB NOT NULL DEFAULT '[]'::jsonb, tone TEXT, audience TEXT, risks JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (article_id, version) ) """, "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS title_options JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS reader_persona TEXT", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS evidence_needs JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS visual_needs JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS seo_notes JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS source_requirements JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS excluded_sources JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS tone TEXT", "ALTER TABLE article_plans ADD COLUMN IF NOT EXISTS audience TEXT", """ CREATE TABLE IF NOT EXISTS plan_sections ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_plan_id UUID NOT NULL REFERENCES article_plans(id) ON DELETE CASCADE, sort_order INTEGER NOT NULL, heading TEXT NOT NULL, purpose TEXT, key_points JSONB NOT NULL DEFAULT '[]'::jsonb, evidence_needs JSONB NOT NULL DEFAULT '[]'::jsonb, claims_to_support JSONB NOT NULL DEFAULT '[]'::jsonb, target_word_count INTEGER, UNIQUE (article_plan_id, sort_order) ) """, "ALTER TABLE plan_sections ADD COLUMN IF NOT EXISTS key_points JSONB NOT NULL DEFAULT '[]'::jsonb", "ALTER TABLE plan_sections ADD COLUMN IF NOT EXISTS evidence_needs JSONB NOT NULL DEFAULT '[]'::jsonb", """ CREATE TABLE IF NOT EXISTS evidence_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, source_title TEXT NOT NULL, source_url TEXT NOT NULL, source_type TEXT NOT NULL, source_quality_score NUMERIC(4, 3) NOT NULL, summary TEXT NOT NULL, supports_claims JSONB NOT NULL DEFAULT '[]'::jsonb, artifact_manifest_id UUID, retrieved_at TIMESTAMPTZ NOT NULL DEFAULT now(), review_status TEXT NOT NULL DEFAULT 'PENDING' ) """, "ALTER TABLE evidence_items ADD COLUMN IF NOT EXISTS review_status TEXT NOT NULL DEFAULT 'PENDING'", f""" CREATE TABLE IF NOT EXISTS claims ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, section_id UUID REFERENCES plan_sections(id) ON DELETE SET NULL, claim_text TEXT NOT NULL, support_status TEXT NOT NULL CHECK ( support_status IN ({CLAIM_SUPPORT_STATUS_VALUES}) ), risk_level TEXT NOT NULL CHECK (risk_level IN ({CLAIM_RISK_LEVEL_VALUES})), evidence_item_ids JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, f""" CREATE TABLE IF NOT EXISTS article_drafts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, version INTEGER NOT NULL, title TEXT NOT NULL, slug TEXT NOT NULL, meta_title TEXT, meta_description TEXT, body_object_key TEXT, status TEXT NOT NULL CHECK (status IN ({ARTICLE_STATUS_VALUES})), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (article_id, version) ) """, f""" CREATE TABLE IF NOT EXISTS assets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, asset_type TEXT NOT NULL CHECK (asset_type IN ({ASSET_TYPE_VALUES})), title TEXT NOT NULL, prompt TEXT, file_url TEXT, alt_text TEXT, caption TEXT, status TEXT NOT NULL CHECK (status IN ({ASSET_STATUS_VALUES})), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, f""" CREATE TABLE IF NOT EXISTS workflow_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, event_type TEXT NOT NULL, from_status TEXT CHECK (from_status IN ({ARTICLE_STATUS_VALUES})), to_status TEXT CHECK (to_status IN ({ARTICLE_STATUS_VALUES})), actor_user_id UUID REFERENCES users(id), payload JSONB NOT NULL DEFAULT '{{}}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, f""" CREATE TABLE IF NOT EXISTS agent_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID REFERENCES articles(id) ON DELETE SET NULL, parent_job_id UUID REFERENCES agent_jobs(id) ON DELETE SET NULL, attempt INTEGER NOT NULL DEFAULT 1, job_type TEXT NOT NULL CHECK (job_type IN ({AGENT_JOB_TYPE_VALUES})), agent_profile TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ({AGENT_JOB_STATUS_VALUES})), workspace_path TEXT, input_files JSONB NOT NULL DEFAULT '[]'::jsonb, output_files JSONB NOT NULL DEFAULT '[]'::jsonb, error_category TEXT CHECK ( error_category IS NULL OR error_category IN ({AGENT_JOB_ERROR_CATEGORY_VALUES}) ), error_message TEXT, stdout TEXT NOT NULL DEFAULT '', stderr TEXT NOT NULL DEFAULT '', exit_code INTEGER, duration_ms INTEGER, queued_at TIMESTAMPTZ NOT NULL DEFAULT now(), started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ ) """, "ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS parent_job_id UUID REFERENCES agent_jobs(id) ON DELETE SET NULL", "ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS attempt INTEGER NOT NULL DEFAULT 1", "ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS stdout TEXT NOT NULL DEFAULT ''", "ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS stderr TEXT NOT NULL DEFAULT ''", "ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS exit_code INTEGER", "ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS duration_ms INTEGER", """ CREATE TABLE IF NOT EXISTS research_run_manifests ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, agent_job_id UUID NOT NULL REFERENCES agent_jobs(id) ON DELETE CASCADE, s3_prefix TEXT NOT NULL, source_urls JSONB NOT NULL DEFAULT '[]'::jsonb, object_keys JSONB NOT NULL DEFAULT '[]'::jsonb, content_hashes JSONB NOT NULL DEFAULT '[]'::jsonb, artifact_types JSONB NOT NULL DEFAULT '[]'::jsonb, metadata_references JSONB NOT NULL DEFAULT '[]'::jsonb, artifacts JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, f""" CREATE TABLE IF NOT EXISTS publish_commits ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES articles(id) ON DELETE CASCADE, target_site_id UUID NOT NULL REFERENCES target_sites(id), repository_url TEXT NOT NULL, branch TEXT NOT NULL, commit_sha TEXT, content_bundle_manifest JSONB NOT NULL DEFAULT '{{}}'::jsonb, status TEXT NOT NULL CHECK (status IN ({PUBLISHING_STATUS_VALUES})), deployment_status TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, f""" CREATE TABLE IF NOT EXISTS prompt_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), prompt_key TEXT NOT NULL, version INTEGER NOT NULL, status TEXT NOT NULL CHECK (status IN ({SCRIPT_CONFIG_STATUS_VALUES})), body TEXT NOT NULL, created_by UUID NOT NULL REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), activated_at TIMESTAMPTZ, diff JSONB NOT NULL DEFAULT '{{}}'::jsonb, UNIQUE (prompt_key, version) ) """, f""" CREATE TABLE IF NOT EXISTS script_config_version_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), target_site_id UUID NOT NULL REFERENCES target_sites(id) ON DELETE CASCADE, version_id UUID NOT NULL REFERENCES script_config_versions(id) ON DELETE CASCADE, event_type TEXT NOT NULL, actor_user_id UUID REFERENCES users(id), payload JSONB NOT NULL DEFAULT '{{}}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, "CREATE INDEX IF NOT EXISTS idx_target_sites_slug ON target_sites (slug)", """ CREATE INDEX IF NOT EXISTS idx_articles_dashboard ON articles (target_site_id, status, publishing_status, updated_at DESC) """, """ CREATE INDEX IF NOT EXISTS idx_articles_editor_dashboard ON articles (assigned_editor_id, updated_at DESC) """, """ CREATE INDEX IF NOT EXISTS idx_agent_jobs_queue ON agent_jobs (status, queued_at) """, """ CREATE INDEX IF NOT EXISTS idx_agent_jobs_article_timeline ON agent_jobs (article_id, job_type, queued_at) """, """ CREATE INDEX IF NOT EXISTS idx_workflow_events_timeline ON workflow_events (article_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_research_run_manifests_article ON research_run_manifests (article_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_publish_commits_article ON publish_commits (article_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_script_config_version_events_site ON script_config_version_events (target_site_id, created_at) """, ) SQLITE_SCHEMA_STATEMENTS: tuple[str, ...] = ( """ CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, role TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS target_sites ( id TEXT PRIMARY KEY, name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, publishing_type TEXT NOT NULL DEFAULT 'git_next', default_language TEXT NOT NULL DEFAULT 'en', brand_voice TEXT NOT NULL, audience TEXT NOT NULL, seo_rules TEXT NOT NULL DEFAULT '{}', visual_rules TEXT NOT NULL DEFAULT '{}', source_rules TEXT NOT NULL DEFAULT '{}', publishing_rules TEXT NOT NULL DEFAULT '{}', active_script_config_version_id TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS script_config_versions ( id TEXT PRIMARY KEY, target_site_id TEXT NOT NULL, version INTEGER NOT NULL, status TEXT NOT NULL, created_by TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, diff TEXT NOT NULL DEFAULT '{}', rollback_target_version_id TEXT, activated_at TEXT, publishing_yaml TEXT NOT NULL, publishing_yaml_hash TEXT NOT NULL, transform_script TEXT NOT NULL, transform_script_hash TEXT NOT NULL, UNIQUE (target_site_id, version) ) """, """ CREATE TABLE IF NOT EXISTS articles ( id TEXT PRIMARY KEY, target_site_id TEXT NOT NULL, status TEXT NOT NULL, publishing_status TEXT NOT NULL, brief_description TEXT NOT NULL, working_title TEXT, language TEXT NOT NULL DEFAULT 'en', content_type TEXT NOT NULL DEFAULT 'longform_guide', primary_keyword TEXT, assigned_editor_id TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS boundary_questions ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, sort_order INTEGER NOT NULL, category TEXT NOT NULL DEFAULT 'general', question TEXT NOT NULL, answer TEXT, is_required INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (article_id, sort_order) ) """, """ CREATE TABLE IF NOT EXISTS article_plans ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, version INTEGER NOT NULL, status TEXT NOT NULL, title_options TEXT NOT NULL DEFAULT '[]', recommended_title TEXT, reader_persona TEXT, search_intent TEXT, thesis TEXT, claims_to_prove TEXT NOT NULL DEFAULT '[]', evidence_needs TEXT NOT NULL DEFAULT '[]', visual_needs TEXT NOT NULL DEFAULT '[]', seo_notes TEXT NOT NULL DEFAULT '[]', source_requirements TEXT NOT NULL DEFAULT '[]', excluded_sources TEXT NOT NULL DEFAULT '[]', tone TEXT, audience TEXT, risks TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (article_id, version) ) """, """ CREATE TABLE IF NOT EXISTS plan_sections ( id TEXT PRIMARY KEY, article_plan_id TEXT NOT NULL, sort_order INTEGER NOT NULL, heading TEXT NOT NULL, purpose TEXT, key_points TEXT NOT NULL DEFAULT '[]', evidence_needs TEXT NOT NULL DEFAULT '[]', claims_to_support TEXT NOT NULL DEFAULT '[]', target_word_count INTEGER, UNIQUE (article_plan_id, sort_order) ) """, """ CREATE TABLE IF NOT EXISTS evidence_items ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, source_title TEXT NOT NULL, source_url TEXT NOT NULL, source_type TEXT NOT NULL, source_quality_score REAL NOT NULL, summary TEXT NOT NULL, supports_claims TEXT NOT NULL DEFAULT '[]', artifact_manifest_id TEXT, retrieved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, review_status TEXT NOT NULL DEFAULT 'PENDING' ) """, """ CREATE TABLE IF NOT EXISTS claims ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, section_id TEXT, claim_text TEXT NOT NULL, support_status TEXT NOT NULL, risk_level TEXT NOT NULL, evidence_item_ids TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS article_drafts ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, version INTEGER NOT NULL, title TEXT NOT NULL, slug TEXT NOT NULL, meta_title TEXT, meta_description TEXT, body_object_key TEXT, status TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (article_id, version) ) """, """ CREATE TABLE IF NOT EXISTS assets ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, asset_type TEXT NOT NULL, title TEXT NOT NULL, prompt TEXT, file_url TEXT, alt_text TEXT, caption TEXT, status TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS workflow_events ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, event_type TEXT NOT NULL, from_status TEXT, to_status TEXT, actor_user_id TEXT, payload TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS agent_jobs ( id TEXT PRIMARY KEY, article_id TEXT, parent_job_id TEXT, attempt INTEGER NOT NULL DEFAULT 1, job_type TEXT NOT NULL, agent_profile TEXT NOT NULL, status TEXT NOT NULL, workspace_path TEXT, input_files TEXT NOT NULL DEFAULT '[]', output_files TEXT NOT NULL DEFAULT '[]', error_category TEXT, error_message TEXT, stdout TEXT NOT NULL DEFAULT '', stderr TEXT NOT NULL DEFAULT '', exit_code INTEGER, duration_ms INTEGER, queued_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, started_at TEXT, finished_at TEXT ) """, """ CREATE TABLE IF NOT EXISTS research_run_manifests ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, agent_job_id TEXT NOT NULL, s3_prefix TEXT NOT NULL, source_urls TEXT NOT NULL DEFAULT '[]', object_keys TEXT NOT NULL DEFAULT '[]', content_hashes TEXT NOT NULL DEFAULT '[]', artifact_types TEXT NOT NULL DEFAULT '[]', metadata_references TEXT NOT NULL DEFAULT '[]', artifacts TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS publish_commits ( id TEXT PRIMARY KEY, article_id TEXT NOT NULL, target_site_id TEXT NOT NULL, repository_url TEXT NOT NULL, branch TEXT NOT NULL, commit_sha TEXT, content_bundle_manifest TEXT NOT NULL DEFAULT '{}', status TEXT NOT NULL, deployment_status TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, """ CREATE TABLE IF NOT EXISTS prompt_versions ( id TEXT PRIMARY KEY, prompt_key TEXT NOT NULL, version INTEGER NOT NULL, status TEXT NOT NULL, body TEXT NOT NULL, created_by TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, activated_at TEXT, diff TEXT NOT NULL DEFAULT '{}', UNIQUE (prompt_key, version) ) """, """ CREATE TABLE IF NOT EXISTS script_config_version_events ( id TEXT PRIMARY KEY, target_site_id TEXT NOT NULL, version_id TEXT NOT NULL, event_type TEXT NOT NULL, actor_user_id TEXT, payload TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) """, "CREATE INDEX IF NOT EXISTS idx_target_sites_slug ON target_sites (slug)", """ CREATE INDEX IF NOT EXISTS idx_articles_dashboard ON articles (target_site_id, status, publishing_status, updated_at) """, """ CREATE INDEX IF NOT EXISTS idx_articles_editor_dashboard ON articles (assigned_editor_id, updated_at) """, """ CREATE INDEX IF NOT EXISTS idx_agent_jobs_queue ON agent_jobs (status, queued_at) """, """ CREATE INDEX IF NOT EXISTS idx_agent_jobs_article_timeline ON agent_jobs (article_id, job_type, queued_at) """, """ CREATE INDEX IF NOT EXISTS idx_workflow_events_timeline ON workflow_events (article_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_research_run_manifests_article ON research_run_manifests (article_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_publish_commits_article ON publish_commits (article_id, created_at) """, """ CREATE INDEX IF NOT EXISTS idx_script_config_version_events_site ON script_config_version_events (target_site_id, created_at) """, ) def schema_statements_for_dsn(dsn: str) -> tuple[str, ...]: if dsn.startswith("sqlite:///"): return SQLITE_SCHEMA_STATEMENTS return POSTGRES_SCHEMA_STATEMENTS def setup_database(dsn: str) -> None: statements = schema_statements_for_dsn(dsn) if dsn.startswith("sqlite:///"): sqlite_path = dsn.removeprefix("sqlite:///") with closing(sqlite3.connect(sqlite_path)) as connection: for statement in statements: connection.execute(statement) connection.commit() return import psycopg with psycopg.connect(dsn) as connection: with connection.cursor() as cursor: for statement in statements: cursor.execute(statement)