feat: добавлена система миграций БД и CI/CD пайплайны
- Создана система отслеживания миграций (MigrationRepository, таблица migrations) - Добавлен скрипт apply_migrations.py для автоматического применения миграций - Созданы CI/CD пайплайны (.github/workflows/ci.yml, deploy.yml) - Обновлена документация по миграциям в database-patterns.md - Миграции применяются автоматически при деплое в продакшн
This commit is contained in:
@@ -9,13 +9,12 @@
|
||||
- async_db: основной класс AsyncBotDB
|
||||
"""
|
||||
|
||||
from .models import (
|
||||
User, BlacklistUser, UserMessage, TelegramPost, PostContent,
|
||||
MessageContentLink, Admin, Migration, AudioMessage, AudioListenRecord, AudioModerate
|
||||
)
|
||||
from .repository_factory import RepositoryFactory
|
||||
from .base import DatabaseConnection
|
||||
from .async_db import AsyncBotDB
|
||||
from .base import DatabaseConnection
|
||||
from .models import (Admin, AudioListenRecord, AudioMessage, AudioModerate,
|
||||
BlacklistUser, MessageContentLink, Migration, PostContent,
|
||||
TelegramPost, User, UserMessage)
|
||||
from .repository_factory import RepositoryFactory
|
||||
|
||||
# Для обратной совместимости экспортируем старый интерфейс
|
||||
__all__ = [
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import aiosqlite
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import aiosqlite
|
||||
from database.models import (Admin, AudioMessage, BlacklistHistoryRecord,
|
||||
BlacklistUser, PostContent, TelegramPost, User,
|
||||
UserMessage)
|
||||
from database.repository_factory import RepositoryFactory
|
||||
from database.models import (
|
||||
User, BlacklistUser, BlacklistHistoryRecord, UserMessage, TelegramPost, PostContent,
|
||||
Admin, AudioMessage
|
||||
)
|
||||
|
||||
|
||||
class AsyncBotDB:
|
||||
@@ -403,25 +403,9 @@ class AsyncBotDB:
|
||||
await self.factory.audio.delete_audio_record_by_file_name(file_name)
|
||||
|
||||
# Методы для миграций
|
||||
async def get_migration_version(self) -> int:
|
||||
"""Получение текущей версии миграции."""
|
||||
return await self.factory.migrations.get_migration_version()
|
||||
|
||||
async def get_current_version(self) -> Optional[int]:
|
||||
"""Возвращает текущую последнюю версию миграции."""
|
||||
return await self.factory.migrations.get_current_version()
|
||||
|
||||
async def update_version(self, new_version: int, script_name: str):
|
||||
"""Обновляет версию миграций в таблице migrations."""
|
||||
await self.factory.migrations.update_version(new_version, script_name)
|
||||
|
||||
async def create_table(self, sql_script: str):
|
||||
"""Создает таблицу в базе. Используется в миграциях."""
|
||||
await self.factory.migrations.create_table(sql_script)
|
||||
|
||||
async def update_migration_version(self, version: int, script_name: str):
|
||||
"""Обновление версии миграции."""
|
||||
await self.factory.migrations.update_version(version, script_name)
|
||||
await self.factory.migrations.create_table_from_sql(sql_script)
|
||||
|
||||
# Методы для voice bot welcome tracking
|
||||
async def check_voice_bot_welcome_received(self, user_id: int) -> bool:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import aiosqlite
|
||||
from typing import Optional
|
||||
|
||||
import aiosqlite
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -89,9 +89,8 @@ class Admin:
|
||||
@dataclass
|
||||
class Migration:
|
||||
"""Модель миграции."""
|
||||
version: int
|
||||
script_name: str
|
||||
created_at: Optional[str] = None
|
||||
applied_at: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -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'
|
||||
]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import Admin
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import BlacklistHistoryRecord
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import UserMessage
|
||||
|
||||
|
||||
79
database/repositories/migration_repository.py
Normal file
79
database/repositories/migration_repository.py
Normal 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)
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from typing import Optional
|
||||
from database.repositories.user_repository import UserRepository
|
||||
from database.repositories.blacklist_repository import BlacklistRepository
|
||||
from database.repositories.blacklist_history_repository import BlacklistHistoryRepository
|
||||
from database.repositories.message_repository import MessageRepository
|
||||
from database.repositories.post_repository import PostRepository
|
||||
|
||||
from database.repositories.admin_repository import AdminRepository
|
||||
from database.repositories.audio_repository import AudioRepository
|
||||
from database.repositories.blacklist_history_repository import \
|
||||
BlacklistHistoryRepository
|
||||
from database.repositories.blacklist_repository import BlacklistRepository
|
||||
from database.repositories.message_repository import MessageRepository
|
||||
from database.repositories.migration_repository import MigrationRepository
|
||||
from database.repositories.post_repository import PostRepository
|
||||
from database.repositories.user_repository import UserRepository
|
||||
|
||||
|
||||
class RepositoryFactory:
|
||||
@@ -20,6 +23,7 @@ class RepositoryFactory:
|
||||
self._post_repo: Optional[PostRepository] = None
|
||||
self._admin_repo: Optional[AdminRepository] = None
|
||||
self._audio_repo: Optional[AudioRepository] = None
|
||||
self._migration_repo: Optional[MigrationRepository] = None
|
||||
|
||||
@property
|
||||
def users(self) -> UserRepository:
|
||||
@@ -70,8 +74,16 @@ class RepositoryFactory:
|
||||
self._audio_repo = AudioRepository(self.db_path)
|
||||
return self._audio_repo
|
||||
|
||||
@property
|
||||
def migrations(self) -> MigrationRepository:
|
||||
"""Возвращает репозиторий миграций."""
|
||||
if self._migration_repo is None:
|
||||
self._migration_repo = MigrationRepository(self.db_path)
|
||||
return self._migration_repo
|
||||
|
||||
async def create_all_tables(self):
|
||||
"""Создает все таблицы в базе данных."""
|
||||
await self.migrations.create_table() # Сначала создаем таблицу миграций
|
||||
await self.users.create_tables()
|
||||
await self.blacklist.create_tables()
|
||||
await self.blacklist_history.create_tables()
|
||||
|
||||
@@ -126,6 +126,13 @@ CREATE TABLE IF NOT EXISTS audio_moderate (
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Database migrations tracking
|
||||
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'))
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
-- Optimized index for user_audio_listens - only user_id for "show all audio listened by user X"
|
||||
CREATE INDEX IF NOT EXISTS idx_user_audio_listens_user_id ON user_audio_listens(user_id);
|
||||
|
||||
Reference in New Issue
Block a user