diff --git a/api/alembic/versions/0004_rehearsal_sessions.py b/api/alembic/versions/0004_rehearsal_sessions.py new file mode 100644 index 0000000..d731c0d --- /dev/null +++ b/api/alembic/versions/0004_rehearsal_sessions.py @@ -0,0 +1,49 @@ +"""Add rehearsal_sessions table, songs.session_id, songs.tags. + +Revision ID: 0004 +Revises: 0003 +Create Date: 2026-03-29 +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import ARRAY, UUID + +revision = "0004" +down_revision = "0003" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "rehearsal_sessions", + sa.Column("id", UUID(as_uuid=True), primary_key=True), + sa.Column("band_id", UUID(as_uuid=True), sa.ForeignKey("bands.id", ondelete="CASCADE"), nullable=False), + sa.Column("date", sa.DateTime(timezone=False), nullable=False), + sa.Column("nc_folder_path", sa.Text()), + sa.Column("label", sa.String(255)), + sa.Column("notes", sa.Text()), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.UniqueConstraint("band_id", "date", name="uq_session_band_date"), + ) + op.create_index("ix_rehearsal_sessions_band_id", "rehearsal_sessions", ["band_id"]) + + op.add_column("songs", sa.Column( + "session_id", UUID(as_uuid=True), + sa.ForeignKey("rehearsal_sessions.id", ondelete="SET NULL"), + nullable=True, + )) + op.create_index("ix_songs_session_id", "songs", ["session_id"]) + + op.add_column("songs", sa.Column( + "tags", ARRAY(sa.Text()), nullable=False, server_default="{}", + )) + + +def downgrade() -> None: + op.drop_column("songs", "tags") + op.drop_index("ix_songs_session_id", table_name="songs") + op.drop_column("songs", "session_id") + op.drop_index("ix_rehearsal_sessions_band_id", table_name="rehearsal_sessions") + op.drop_table("rehearsal_sessions") diff --git a/api/src/rehearsalhub/db/models.py b/api/src/rehearsalhub/db/models.py index 67be24d..af9460e 100644 --- a/api/src/rehearsalhub/db/models.py +++ b/api/src/rehearsalhub/db/models.py @@ -84,6 +84,9 @@ class Band(Base): songs: Mapped[list[Song]] = relationship( "Song", back_populates="band", cascade="all, delete-orphan" ) + sessions: Mapped[list[RehearsalSession]] = relationship( + "RehearsalSession", back_populates="band", cascade="all, delete-orphan" + ) class BandMember(Base): @@ -128,6 +131,29 @@ class BandInvite(Base): creator: Mapped[Member] = relationship("Member", foreign_keys=[created_by]) +# ── Rehearsal Sessions ──────────────────────────────────────────────────────── + + +class RehearsalSession(Base): + __tablename__ = "rehearsal_sessions" + __table_args__ = (UniqueConstraint("band_id", "date", name="uq_session_band_date"),) + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + band_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("bands.id", ondelete="CASCADE"), nullable=False, index=True + ) + date: Mapped[datetime] = mapped_column(DateTime(timezone=False), nullable=False) + nc_folder_path: Mapped[Optional[str]] = mapped_column(Text) + label: Mapped[Optional[str]] = mapped_column(String(255)) + notes: Mapped[Optional[str]] = mapped_column(Text) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + + band: Mapped[Band] = relationship("Band", back_populates="sessions") + songs: Mapped[list[Song]] = relationship("Song", back_populates="session") + + # ── Songs ───────────────────────────────────────────────────────────────────── @@ -138,9 +164,13 @@ class Song(Base): band_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("bands.id", ondelete="CASCADE"), nullable=False, index=True ) + session_id: Mapped[Optional[uuid.UUID]] = mapped_column( + UUID(as_uuid=True), ForeignKey("rehearsal_sessions.id", ondelete="SET NULL"), index=True + ) title: Mapped[str] = mapped_column(String(500), nullable=False) nc_folder_path: Mapped[Optional[str]] = mapped_column(Text) status: Mapped[str] = mapped_column(String(20), nullable=False, default="jam") + tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list, nullable=False) global_key: Mapped[Optional[str]] = mapped_column(String(30)) global_bpm: Mapped[Optional[float]] = mapped_column(Numeric(6, 2)) notes: Mapped[Optional[str]] = mapped_column(Text) @@ -155,6 +185,7 @@ class Song(Base): ) band: Mapped[Band] = relationship("Band", back_populates="songs") + session: Mapped[Optional[RehearsalSession]] = relationship("RehearsalSession", back_populates="songs") creator: Mapped[Optional[Member]] = relationship("Member", back_populates="authored_songs") versions: Mapped[list[AudioVersion]] = relationship( "AudioVersion", back_populates="song", cascade="all, delete-orphan"