"""v1 second_brain: sources, extractions, wiki_pages Revision ID: 4a1e2f6c9d10 Revises: Create Date: 2026-05-24 00:00:00.000000 Emits the v1 second_brain schema: the relational backbone of the extraction pipeline. Enum types (`source_type`, `source_status`) live inside the schema so they don't collide with anything else in the petalbrain cluster. env.py creates the second_brain schema and pins alembic_version to it before this migration runs. Below we re-issue CREATE SCHEMA IF NOT EXISTS defensively and SET search_path so unqualified names resolve. Vector indexing lives in public.embeddings — managed out-of-band, not by this migration tree. """ from __future__ import annotations from collections.abc import Sequence from alembic import op revision: str = "4a1e2f6c9d10" down_revision: str | Sequence[str] | None = None branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: """Create the v1 second_brain schema.""" # The schema is bootstrapped by env.py (only if missing); skip the # IF NOT EXISTS form here because lovebug doesn't have CREATE on the # petalbrain database and the bare statement errors out even when the # schema already exists. Just SET search_path and emit DDL inside it. op.execute("SET search_path TO second_brain") op.execute( "CREATE TYPE source_type AS ENUM ('video', 'article')" ) op.execute( """ CREATE TYPE source_status AS ENUM ( 'pending', 'pulled', 'transcribed', 'analyzed', 'accepted', 'published', 'failed' ) """ ) op.execute( """ CREATE TABLE sources ( id SERIAL PRIMARY KEY, url VARCHAR(2000) NOT NULL UNIQUE, title VARCHAR(500), source_type source_type NOT NULL DEFAULT 'video', domain VARCHAR(50) NOT NULL DEFAULT 'development', focus TEXT, status source_status NOT NULL DEFAULT 'pending', media_path VARCHAR(1000), transcript_path VARCHAR(1000), transcript_text TEXT, published_at TIMESTAMP, duration_seconds INTEGER, author VARCHAR(200), ingested_at TIMESTAMP NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'), updated_at TIMESTAMP, error_message TEXT ) """ ) op.execute("CREATE INDEX idx_sources_status ON sources(status)") op.execute("CREATE INDEX idx_sources_domain ON sources(domain)") op.execute( """ CREATE TABLE extractions ( id SERIAL PRIMARY KEY, source_id INTEGER NOT NULL UNIQUE REFERENCES sources(id) ON DELETE CASCADE, summary TEXT, key_points JSON, entities JSON, claims JSON, open_questions JSON, contradictions JSON, raw_response TEXT, input_tokens INTEGER, output_tokens INTEGER, created_at TIMESTAMP NOT NULL DEFAULT (now() AT TIME ZONE 'UTC') ) """ ) op.execute( """ CREATE TABLE wiki_pages ( id SERIAL PRIMARY KEY, vault_path VARCHAR(500) NOT NULL UNIQUE, domain VARCHAR(50) NOT NULL, title VARCHAR(300) NOT NULL, git_sha_before VARCHAR(40), git_sha_after VARCHAR(40), created_at TIMESTAMP NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'), updated_at TIMESTAMP ) """ ) def downgrade() -> None: """Drop the v1 second_brain tables and enums (schema is left intact).""" op.execute("SET search_path TO second_brain") op.execute("DROP TABLE IF EXISTS wiki_pages") op.execute("DROP TABLE IF EXISTS extractions") op.execute("DROP TABLE IF EXISTS sources") op.execute("DROP TYPE IF EXISTS source_status") op.execute("DROP TYPE IF EXISTS source_type")