Files
content-factory/apps/backend/src/infrastructure/schema.py
T

694 lines
26 KiB
Python

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)