feat: добавлена система миграций БД и CI/CD пайплайны

- Создана система отслеживания миграций (MigrationRepository, таблица migrations)
- Добавлен скрипт apply_migrations.py для автоматического применения миграций
- Созданы CI/CD пайплайны (.github/workflows/ci.yml, deploy.yml)
- Обновлена документация по миграциям в database-patterns.md
- Миграции применяются автоматически при деплое в продакшн
This commit is contained in:
2026-01-25 23:17:09 +03:00
parent 07e72c4d14
commit e2b1353408
109 changed files with 1342 additions and 1441 deletions

View File

@@ -9,17 +9,20 @@
- post_repository: работа с постами
- admin_repository: работа с администраторами
- audio_repository: работа с аудио
- migration_repository: работа с миграциями БД
"""
from .user_repository import UserRepository
from .blacklist_repository import BlacklistRepository
from .blacklist_history_repository import BlacklistHistoryRepository
from .message_repository import MessageRepository
from .post_repository import PostRepository
from .admin_repository import AdminRepository
from .audio_repository import AudioRepository
from .blacklist_history_repository import BlacklistHistoryRepository
from .blacklist_repository import BlacklistRepository
from .message_repository import MessageRepository
from .migration_repository import MigrationRepository
from .post_repository import PostRepository
from .user_repository import UserRepository
__all__ = [
'UserRepository', 'BlacklistRepository', 'BlacklistHistoryRepository',
'MessageRepository', 'PostRepository', 'AdminRepository', 'AudioRepository'
'MessageRepository', 'PostRepository', 'AdminRepository', 'AudioRepository',
'MigrationRepository'
]

View File

@@ -1,4 +1,5 @@
from typing import Optional
from database.base import DatabaseConnection
from database.models import Admin

View File

@@ -1,7 +1,8 @@
from typing import Optional, List, Dict, Any
from database.base import DatabaseConnection
from database.models import AudioMessage, AudioListenRecord, AudioModerate
from datetime import datetime
from typing import Any, Dict, List, Optional
from database.base import DatabaseConnection
from database.models import AudioListenRecord, AudioMessage, AudioModerate
class AudioRepository(DatabaseConnection):

View File

@@ -1,4 +1,5 @@
from typing import Optional
from database.base import DatabaseConnection
from database.models import BlacklistHistoryRecord

View File

@@ -1,4 +1,5 @@
from typing import Optional, List, Dict
from typing import Dict, List, Optional
from database.base import DatabaseConnection
from database.models import BlacklistUser

View File

@@ -1,5 +1,6 @@
from datetime import datetime
from typing import Optional
from database.base import DatabaseConnection
from database.models import UserMessage

View File

@@ -0,0 +1,79 @@
"""Репозиторий для работы с миграциями базы данных."""
import aiosqlite
from database.base import DatabaseConnection
class MigrationRepository(DatabaseConnection):
"""Репозиторий для управления миграциями базы данных."""
async def create_table(self):
"""Создает таблицу migrations, если она не существует."""
query = """
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
script_name TEXT NOT NULL UNIQUE,
applied_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
)
"""
await self._execute_query(query)
self.logger.info("Таблица migrations создана или уже существует")
async def get_applied_migrations(self) -> list[str]:
"""Возвращает список имен примененных скриптов миграций."""
conn = None
try:
conn = await self._get_connection()
cursor = await conn.execute("SELECT script_name FROM migrations ORDER BY applied_at")
rows = await cursor.fetchall()
await cursor.close()
return [row[0] for row in rows]
except Exception as e:
self.logger.error(f"Ошибка при получении списка миграций: {e}")
raise
finally:
if conn:
await conn.close()
async def is_migration_applied(self, script_name: str) -> bool:
"""Проверяет, применена ли миграция."""
conn = None
try:
conn = await self._get_connection()
cursor = await conn.execute(
"SELECT COUNT(*) FROM migrations WHERE script_name = ?",
(script_name,)
)
row = await cursor.fetchone()
await cursor.close()
return row[0] > 0 if row else False
except Exception as e:
self.logger.error(f"Ошибка при проверке миграции {script_name}: {e}")
raise
finally:
if conn:
await conn.close()
async def mark_migration_applied(self, script_name: str) -> None:
"""Отмечает миграцию как примененную."""
conn = None
try:
conn = await self._get_connection()
await conn.execute(
"INSERT INTO migrations (script_name) VALUES (?)",
(script_name,)
)
await conn.commit()
self.logger.info(f"Миграция {script_name} отмечена как примененная")
except aiosqlite.IntegrityError:
self.logger.warning(f"Миграция {script_name} уже была применена ранее")
except Exception as e:
self.logger.error(f"Ошибка при отметке миграции {script_name}: {e}")
raise
finally:
if conn:
await conn.close()
async def create_table_from_sql(self, sql_script: str) -> None:
"""Создает таблицу из SQL скрипта. Используется в миграциях."""
await self._execute_query(sql_script)

View File

@@ -1,7 +1,8 @@
from datetime import datetime
from typing import Optional, List, Tuple
from typing import List, Optional, Tuple
from database.base import DatabaseConnection
from database.models import TelegramPost, PostContent, MessageContentLink
from database.models import MessageContentLink, PostContent, TelegramPost
class PostRepository(DatabaseConnection):

View File

@@ -1,5 +1,6 @@
from datetime import datetime
from typing import Optional, List, Dict, Any
from typing import Any, Dict, List, Optional
from database.base import DatabaseConnection
from database.models import User