diff --git a/alembic/versions/005_news_items.py b/alembic/versions/005_news_items.py new file mode 100644 index 0000000..08e9afc --- /dev/null +++ b/alembic/versions/005_news_items.py @@ -0,0 +1,64 @@ +"""news_items table + article_summaries.model + +Revision ID: 005_news_items +Revises: 004_camera_enum +Create Date: 2026-08-28 +""" + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "005_news_items" +down_revision = "004_camera_enum" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Idempotent DDL: ingest (summarizer ensure_tables) may create the same + # shapes first depending on container startup order. IF NOT EXISTS makes + # both orders safe — whichever runs first wins, the other no-ops. + # One statement per op.execute: asyncpg rejects multi-command prepared + # statements (same style as 003_news). + op.execute( + """ + ALTER TABLE article_summaries + ADD COLUMN IF NOT EXISTS model TEXT + """ + ) + op.execute( + """ + CREATE TABLE IF NOT EXISTS news_items ( + id SERIAL PRIMARY KEY, + summary_id INTEGER REFERENCES article_summaries(id) ON DELETE CASCADE, + kind TEXT NOT NULL, + headline TEXT NOT NULL, + importance TEXT NOT NULL, + location_name TEXT, + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + location_confidence TEXT, + category TEXT, + url TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ) + """ + ) + op.execute( + """ + CREATE INDEX IF NOT EXISTS ix_news_items_kind_created + ON news_items (kind, created_at DESC) + """ + ) + op.execute( + """ + CREATE INDEX IF NOT EXISTS ix_news_items_map_bbox + ON news_items (lon, lat) + WHERE kind = 'map' AND lat IS NOT NULL AND lon IS NOT NULL + """ + ) + + +def downgrade() -> None: + op.execute("DROP TABLE IF EXISTS news_items") + op.execute("ALTER TABLE article_summaries DROP COLUMN IF EXISTS model") diff --git a/app/models.py b/app/models.py index 706b5a2..b8ad2b2 100644 --- a/app/models.py +++ b/app/models.py @@ -185,8 +185,8 @@ Index("ix_fires_bbox", fires.c.longitude, fires.c.latitude) # ── News pipeline (scraper + summarizer) ────────────────────────────────── -# Written by the vendored news-scraper (Scrapy) / news-summarizer (Gemini) -# services; schema must match the idempotent alembic migration 003_news. +# Written by the vendored news-scraper (Scrapy) / news-summarizer services; +# schema must match the idempotent alembic migrations 003_news + 005_news_items. articles = Table( "articles", @@ -209,6 +209,26 @@ article_summaries = Table( Column("summary_text", Text, nullable=False), Column("batch_timestamp", DateTime(timezone=True), server_default=func.now(), nullable=False), + Column("model", Text), # LLM id used for this batch; nullable for old rows ) Index("ix_article_summaries_batch_timestamp", article_summaries.c.batch_timestamp) + + +news_items = Table( + "news_items", + metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("summary_id", Integer), + Column("kind", Text, nullable=False), + Column("headline", Text, nullable=False), + Column("importance", Text, nullable=False), + Column("location_name", Text), + Column("lat", Float), + Column("lon", Float), + Column("location_confidence", Text), + Column("category", Text), + Column("url", Text), + Column("created_at", DateTime(timezone=True), server_default=func.now(), nullable=False), +) +Index("ix_news_items_kind_created", news_items.c.kind, news_items.c.created_at)