diff --git a/IMPLEMENTATION_STATUS.md b/IMPLEMENTATION_STATUS.md index b256953..e140bf1 100644 --- a/IMPLEMENTATION_STATUS.md +++ b/IMPLEMENTATION_STATUS.md @@ -63,12 +63,13 @@ ## КРИТИЧЕСКИЕ ИСПРАВЛЕНИЯ -### Migrations Strategy -**Решение**: Миграции запускаются в Dockerfile перед стартом сервера +### Migrations Strategy - Чистый Alembic +**Решение**: Только Alembic управляет схемой БД, без `create_all()` **Файлы**: +- `backend/alembic/versions/2025_12_31_1400-001_initial_schema.py` - инициализирующая миграция (users, folders, assets, shares) - `backend/Dockerfile:34` - `alembic upgrade head` перед uvicorn -- `backend/Dockerfile:25-26` - Copy alembic files to container -- Удален `run_migrations()` из кода приложения (clean separation) +- `backend/main.py:17` - убран вызов `init_db()` (только Alembic) +- `backend/database.py:53` - `init_db()` помечен WARNING (только для тестов) --- diff --git a/backend/alembic/versions/2025_12_31_1200-001_add_folders_and_asset_folder_relationship.py b/backend/alembic/versions/2025_12_31_1200-001_add_folders_and_asset_folder_relationship.py deleted file mode 100644 index 0864d41..0000000 --- a/backend/alembic/versions/2025_12_31_1200-001_add_folders_and_asset_folder_relationship.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Add folders and asset folder relationship - -Revision ID: 001 -Revises: -Create Date: 2025-12-31 12:00:00.000000 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '001' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Create folders table and add folder_id to assets.""" - # Create folders table - op.create_table( - 'folders', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('user_id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('parent_folder_id', sa.String(length=36), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_folders_user_id'), 'folders', ['user_id'], unique=False) - op.create_index(op.f('ix_folders_parent_folder_id'), 'folders', ['parent_folder_id'], unique=False) - - # Add folder_id column to assets table - op.add_column('assets', sa.Column('folder_id', sa.String(length=36), nullable=True)) - op.create_index(op.f('ix_assets_folder_id'), 'assets', ['folder_id'], unique=False) - - -def downgrade() -> None: - """Remove folder_id from assets and drop folders table.""" - # Remove folder_id from assets - op.drop_index(op.f('ix_assets_folder_id'), table_name='assets') - op.drop_column('assets', 'folder_id') - - # Drop folders table - op.drop_index(op.f('ix_folders_parent_folder_id'), table_name='folders') - op.drop_index(op.f('ix_folders_user_id'), table_name='folders') - op.drop_table('folders') diff --git a/backend/alembic/versions/2025_12_31_1400-001_initial_schema.py b/backend/alembic/versions/2025_12_31_1400-001_initial_schema.py new file mode 100644 index 0000000..e0ef73a --- /dev/null +++ b/backend/alembic/versions/2025_12_31_1400-001_initial_schema.py @@ -0,0 +1,101 @@ +"""Initial schema + +Revision ID: 001 +Revises: +Create Date: 2025-12-31 14:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '001' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Create initial database schema.""" + # Create users table + op.create_table( + 'users', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('password_hash', sa.String(length=255), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + + # Create folders table + op.create_table( + 'folders', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('user_id', sa.String(length=36), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('parent_folder_id', sa.String(length=36), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_folders_user_id'), 'folders', ['user_id'], unique=False) + op.create_index(op.f('ix_folders_parent_folder_id'), 'folders', ['parent_folder_id'], unique=False) + + # Create assets table + op.create_table( + 'assets', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('user_id', sa.String(length=36), nullable=False), + sa.Column('folder_id', sa.String(length=36), nullable=True), + sa.Column('type', sa.Enum('photo', 'video', name='assettype'), nullable=False), + sa.Column('status', sa.Enum('uploading', 'ready', 'failed', name='assetstatus'), nullable=False), + sa.Column('original_filename', sa.String(length=512), nullable=False), + sa.Column('content_type', sa.String(length=100), nullable=False), + sa.Column('size_bytes', sa.BigInteger(), nullable=False), + sa.Column('sha256', sa.String(length=64), nullable=True), + sa.Column('captured_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('width', sa.Integer(), nullable=True), + sa.Column('height', sa.Integer(), nullable=True), + sa.Column('duration_sec', sa.Float(), nullable=True), + sa.Column('storage_key_original', sa.String(length=512), nullable=False), + sa.Column('storage_key_thumb', sa.String(length=512), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_assets_user_id'), 'assets', ['user_id'], unique=False) + op.create_index(op.f('ix_assets_folder_id'), 'assets', ['folder_id'], unique=False) + op.create_index(op.f('ix_assets_status'), 'assets', ['status'], unique=False) + op.create_index(op.f('ix_assets_created_at'), 'assets', ['created_at'], unique=False) + + # Create shares table + op.create_table( + 'shares', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('owner_user_id', sa.String(length=36), nullable=False), + sa.Column('asset_id', sa.String(length=36), nullable=True), + sa.Column('album_id', sa.String(length=36), nullable=True), + sa.Column('token', sa.String(length=64), nullable=False), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('password_hash', sa.String(length=255), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_shares_owner_user_id'), 'shares', ['owner_user_id'], unique=False) + op.create_index(op.f('ix_shares_asset_id'), 'shares', ['asset_id'], unique=False) + op.create_index(op.f('ix_shares_album_id'), 'shares', ['album_id'], unique=False) + op.create_index(op.f('ix_shares_token'), 'shares', ['token'], unique=True) + op.create_index(op.f('ix_shares_revoked_at'), 'shares', ['revoked_at'], unique=False) + + +def downgrade() -> None: + """Drop all tables.""" + op.drop_table('shares') + op.drop_table('assets') + op.drop_table('folders') + op.drop_table('users')