The runtime role (lovebug) doesn't have CREATE on the petalbrain database even though it owns the second_brain schema, so a bare `CREATE SCHEMA IF NOT EXISTS` errors out with permission denied. Gate the bootstrap on a pg_namespace lookup so we only attempt the create when the schema is genuinely missing — operators bootstrap it once as postgres superuser, alembic just respects it afterward. The smoke test exercises the full Postgres + embedding path against a live DB + Ollama (autoskipped otherwise): writes an extraction, embeds the summary, asserts public.embeddings has the expected row count, and re-embeds to verify the delete-before-insert idempotency.
121 lines
4.1 KiB
Python
121 lines
4.1 KiB
Python
"""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")
|