"""add moments + junction tables + moment embeddings Revision ID: 0041 Revises: 0040 Create Date: 2026-04-25 People, Places, Tasks, and Notes all live in the `notes` table (distinguished by note_type and is_task). The four junction tables FK to notes(id) but stay separate so per-link-kind queries don't require a discriminator filter. """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects.postgresql import ARRAY, JSONB revision = "0041" down_revision = "0040" branch_labels = None depends_on = None def upgrade() -> None: op.create_table( "moments", sa.Column("id", sa.Integer, primary_key=True), sa.Column( "user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ), sa.Column( "conversation_id", sa.Integer, sa.ForeignKey("conversations.id", ondelete="SET NULL"), nullable=True, ), sa.Column( "source_message_id", sa.Integer, sa.ForeignKey("messages.id", ondelete="SET NULL"), nullable=True, ), sa.Column("day_date", sa.Date, nullable=False), sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=False), sa.Column( "recorded_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), sa.Column("content", sa.Text, nullable=False), sa.Column("raw_excerpt", sa.Text, nullable=True), sa.Column( "tags", ARRAY(sa.Text), nullable=False, server_default=sa.text("'{}'::text[]"), ), sa.Column( "pinned", sa.Boolean, nullable=False, server_default=sa.text("false"), ), ) op.create_index("ix_moments_user_day", "moments", ["user_id", "day_date"]) op.create_index("ix_moments_user_occurred", "moments", ["user_id", "occurred_at"]) # Four junction tables, all FK to notes(id). Separate (vs. one merged # table with a discriminator) so per-kind queries don't need a filter. for table_name, fk_col in [ ("moment_people", "person_id"), ("moment_places", "place_id"), ("moment_tasks", "task_id"), ("moment_notes", "note_id"), ]: op.create_table( table_name, sa.Column( "moment_id", sa.Integer, sa.ForeignKey("moments.id", ondelete="CASCADE"), primary_key=True, ), sa.Column( fk_col, sa.Integer, sa.ForeignKey("notes.id", ondelete="CASCADE"), primary_key=True, ), ) op.create_index(f"ix_{table_name}_{fk_col}", table_name, [fk_col]) op.create_table( "moment_embeddings", sa.Column( "moment_id", sa.Integer, sa.ForeignKey("moments.id", ondelete="CASCADE"), primary_key=True, ), sa.Column("user_id", sa.Integer, nullable=False), sa.Column("embedding", JSONB, nullable=False), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), ) op.create_index("ix_moment_embeddings_user", "moment_embeddings", ["user_id"]) def downgrade() -> None: op.drop_index("ix_moment_embeddings_user", table_name="moment_embeddings") op.drop_table("moment_embeddings") for table_name, fk_col in [ ("moment_notes", "note_id"), ("moment_tasks", "task_id"), ("moment_places", "place_id"), ("moment_people", "person_id"), ]: op.drop_index(f"ix_{table_name}_{fk_col}", table_name=table_name) op.drop_table(table_name) op.drop_index("ix_moments_user_occurred", table_name="moments") op.drop_index("ix_moments_user_day", table_name="moments") op.drop_table("moments")