diff --git a/alembic/versions/0041_add_moments.py b/alembic/versions/0041_add_moments.py new file mode 100644 index 0000000..c1a9f6e --- /dev/null +++ b/alembic/versions/0041_add_moments.py @@ -0,0 +1,128 @@ +"""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")