From 2a809d0418e394fe094f2ae3add4482b687be3fe Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 1 Apr 2026 11:12:21 +0200 Subject: [PATCH] fix: make migration 109 idempotent --- .../alembic/versions/109_add_folders_table.py | 120 ++++++++++-------- 1 file changed, 65 insertions(+), 55 deletions(-) diff --git a/surfsense_backend/alembic/versions/109_add_folders_table.py b/surfsense_backend/alembic/versions/109_add_folders_table.py index f25062617..447679165 100644 --- a/surfsense_backend/alembic/versions/109_add_folders_table.py +++ b/surfsense_backend/alembic/versions/109_add_folders_table.py @@ -11,6 +11,7 @@ index to correctly handle NULL parent_id at root level. from collections.abc import Sequence import sqlalchemy as sa +from sqlalchemy import inspect from alembic import op @@ -21,67 +22,76 @@ depends_on: str | Sequence[str] | None = None def upgrade() -> None: - op.create_table( - "folders", - sa.Column("id", sa.Integer(), primary_key=True, index=True), - sa.Column("name", sa.String(255), nullable=False, index=True), - sa.Column("position", sa.String(50), nullable=False, index=True), - sa.Column( - "parent_id", - sa.Integer(), - sa.ForeignKey("folders.id", ondelete="CASCADE"), - nullable=True, - index=True, - ), - sa.Column( - "search_space_id", - sa.Integer(), - sa.ForeignKey("searchspaces.id", ondelete="CASCADE"), - nullable=False, - index=True, - ), - sa.Column( - "created_by_id", - sa.Uuid(), - sa.ForeignKey("user.id", ondelete="SET NULL"), - nullable=True, - index=True, - ), - sa.Column( - "created_at", - sa.TIMESTAMP(timezone=True), - nullable=False, - server_default=sa.func.now(), - ), - sa.Column( - "updated_at", - sa.TIMESTAMP(timezone=True), - nullable=False, - server_default=sa.func.now(), - ), - ) + conn = op.get_bind() + inspector = inspect(conn) + existing_tables = inspector.get_table_names() + + if "folders" not in existing_tables: + op.create_table( + "folders", + sa.Column("id", sa.Integer(), primary_key=True, index=True), + sa.Column("name", sa.String(255), nullable=False, index=True), + sa.Column("position", sa.String(50), nullable=False, index=True), + sa.Column( + "parent_id", + sa.Integer(), + sa.ForeignKey("folders.id", ondelete="CASCADE"), + nullable=True, + index=True, + ), + sa.Column( + "search_space_id", + sa.Integer(), + sa.ForeignKey("searchspaces.id", ondelete="CASCADE"), + nullable=False, + index=True, + ), + sa.Column( + "created_by_id", + sa.Uuid(), + sa.ForeignKey("user.id", ondelete="SET NULL"), + nullable=True, + index=True, + ), + sa.Column( + "created_at", + sa.TIMESTAMP(timezone=True), + nullable=False, + server_default=sa.func.now(), + ), + sa.Column( + "updated_at", + sa.TIMESTAMP(timezone=True), + nullable=False, + server_default=sa.func.now(), + ), + ) # Expression-based unique index: COALESCE(parent_id, 0) handles NULL correctly. # PostgreSQL treats NULL != NULL in regular unique constraints, so a standard # UniqueConstraint(search_space_id, parent_id, name) would allow duplicate # folder names at the root level. - op.execute( - """ - CREATE UNIQUE INDEX uq_folder_space_parent_name - ON folders (search_space_id, COALESCE(parent_id, 0), name); - """ - ) + existing_indexes = [idx["name"] for idx in inspector.get_indexes("folders")] + if "uq_folder_space_parent_name" not in existing_indexes: + op.execute( + """ + CREATE UNIQUE INDEX uq_folder_space_parent_name + ON folders (search_space_id, COALESCE(parent_id, 0), name); + """ + ) - op.add_column( - "documents", - sa.Column( - "folder_id", - sa.Integer(), - sa.ForeignKey("folders.id", ondelete="SET NULL"), - nullable=True, - index=True, - ), - ) + existing_columns = [col["name"] for col in inspector.get_columns("documents")] + if "folder_id" not in existing_columns: + op.add_column( + "documents", + sa.Column( + "folder_id", + sa.Integer(), + sa.ForeignKey("folders.id", ondelete="SET NULL"), + nullable=True, + index=True, + ), + ) def downgrade() -> None: