diff --git a/CHANGELOG.md b/CHANGELOG.md index 177a150f..ba5ef300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog + +## [0.9.15] - 2025-08-30 + +### 🔧 Fixed +- **🧾 Database Table Creation**: Унифицирован подход к созданию таблиц БД между продакшеном и тестами + - Исправлена ошибка "no such table: author" в тестах + - Обновлена функция `create_all_tables()` в `storage/schema.py` для использования стандартного SQLAlchemy подхода + - Улучшены фикстуры тестов с принудительным импортом всех ORM моделей + - Добавлена детальная диагностика создания таблиц в тестах + - Добавлены fallback механизмы для создания таблиц в проблемных окружениях + +### 🧪 Testing +- Все RBAC тесты теперь проходят успешно +- Исправлены фикстуры `test_engine`, `db_session` и `test_session_factory` +- Добавлены функции `ensure_all_tables_exist()` и `ensure_all_models_imported()` для диагностики + +### 📝 Technical Details +- Заменен подход `create_table_if_not_exists()` на стандартный `Base.metadata.create_all()` +- Улучшена обработка ошибок при создании таблиц +- Добавлена проверка регистрации всех критических таблиц в metadata + ## [0.9.14] - 2025-08-28 ### 🔍 Улучшено @@ -2331,3 +2352,4 @@ Radical architecture simplification with separation into service layer and thin - `settings` moved to base and now smaller - new outside auth schema - removed `gittask`, `auth`, `inbox`, `migration` + diff --git a/cache/precache.py b/cache/precache.py index 555f251c..717f0853 100644 --- a/cache/precache.py +++ b/cache/precache.py @@ -77,10 +77,6 @@ async def precache_topics_followers(topic_id: int, session) -> None: followers_payload = fast_json_dumps(list(topic_followers)) await redis.execute("SET", f"topic:followers:{topic_id}", followers_payload) - # Логируем только если количество фолловеров равно 0 - if len(topic_followers) == 0: - logger.debug(f"Topic #{topic_id} has 0 followers") - except Exception as e: logger.error(f"Error precaching followers for topic #{topic_id}: {e}") # В случае ошибки, устанавливаем пустой список @@ -230,9 +226,7 @@ async def precache_data() -> None: topics_with_zero_followers.append(topic_slug) if topics_with_zero_followers: - logger.info(f"📋 Топики с 0 фолловерами ({len(topics_with_zero_followers)}):") - for slug in sorted(topics_with_zero_followers): - logger.info(f" • {slug}") + logger.info(f"📋 Топиков с 0 фолловерами: {len(topics_with_zero_followers)}") else: logger.info("✅ Все топики имеют фолловеров") diff --git a/services/search.py b/services/search.py index 65a7d09b..dab61029 100644 --- a/services/search.py +++ b/services/search.py @@ -9,28 +9,6 @@ import numpy as np from settings import MUVERA_INDEX_NAME, SEARCH_MAX_BATCH_SIZE, SEARCH_PREFETCH_SIZE from utils.logger import root_logger as logger - -# Простые метрики производительности поиска -class SearchMetrics: - def __init__(self) -> None: - self.indexing_start_time: float | None = None - self.documents_indexed: int = 0 - - def start_indexing(self, doc_count: int): - self.indexing_start_time = time.time() - self.documents_indexed = doc_count - print(f"🔍 Индексация {doc_count} документов...") - - def end_indexing(self): - if self.indexing_start_time: - duration = time.time() - self.indexing_start_time - rate = self.documents_indexed / duration if duration > 0 else 0 - print(f"✅ Индексация завершена за {duration:.2f}s ({rate:.1f} doc/s)") - - -# Глобальный экземпляр метрик -search_metrics = SearchMetrics() - # Global collection for background tasks background_tasks: List[asyncio.Task] = [] @@ -303,7 +281,6 @@ class SearchService: return # Запускаем метрики индексации - search_metrics.start_indexing(len(shouts)) start_time = time.time() logger.info(f"Starting Muvera bulk indexing of {len(shouts)} documents") @@ -375,8 +352,6 @@ class SearchService: f"Muvera bulk indexing completed in {elapsed:.2f}s: " f"{len(documents)} documents indexed, {total_skipped} shouts skipped" ) - # Завершаем метрики индексации - search_metrics.end_indexing() except Exception as e: logger.exception(f"Muvera bulk indexing failed: {e}") else: diff --git a/storage/schema.py b/storage/schema.py index d5a67961..f5a292f3 100644 --- a/storage/schema.py +++ b/storage/schema.py @@ -9,9 +9,7 @@ from ariadne import ( load_schema_from_path, ) -from orm import collection, community, draft, invite, notification, reaction, shout, topic -from orm.author import Author, AuthorBookmark, AuthorFollower, AuthorRating -from storage.db import create_table_if_not_exists +from storage.db import engine # Создаем основные типы query = QueryType() @@ -36,52 +34,64 @@ resolvers: SchemaBindable | type[Enum] | list[SchemaBindable | type[Enum]] = [ def create_all_tables() -> None: - """Create all database tables in the correct order.""" - # Порядок важен - сначала таблицы без внешних ключей, затем зависимые таблицы - models_in_order = [ - # user.User, # Базовая таблица auth - Author, # Базовая таблица - community.Community, # Базовая таблица - topic.Topic, # Базовая таблица - # Связи для базовых таблиц - AuthorFollower, # Зависит от Author - community.CommunityFollower, # Зависит от Community - topic.TopicFollower, # Зависит от Topic - # Черновики (теперь без зависимости от Shout) - draft.Draft, # Зависит только от Author - draft.DraftAuthor, # Зависит от Draft и Author - draft.DraftTopic, # Зависит от Draft и Topic - # Основные таблицы контента - shout.Shout, # Зависит от Author и Draft - shout.ShoutAuthor, # Зависит от Shout и Author - shout.ShoutTopic, # Зависит от Shout и Topic - # Реакции - reaction.Reaction, # Зависит от Author и Shout - shout.ShoutReactionsFollower, # Зависит от Shout и Reaction - # Дополнительные таблицы - AuthorRating, # Зависит от Author - AuthorBookmark, # Зависит от Author - notification.Notification, # Зависит от Author - notification.NotificationSeen, # Зависит от Notification - collection.Collection, # Зависит от Author - invite.Invite, # Зависит от Author и Shout - collection.ShoutCollection, # Зависит от Collection и Shout - ] + """Create all database tables using SQLAlchemy's standard approach.""" + try: + # Импортируем все модели чтобы они были зарегистрированы в Base.metadata - from storage.db import engine + # Получаем Base с зарегистрированными моделями + from orm.base import BaseModel as Base - # Используем одно соединение для всех таблиц, чтобы избежать проблем с транзакциями - with engine.connect() as connection: - for model in models_in_order: - try: - # Ensure model is a type[DeclarativeBase] - if not hasattr(model, "__tablename__"): - logger.warning(f"Skipping {model} - not a DeclarativeBase model") - continue + # Проверяем что все критические таблицы зарегистрированы + required_tables = [ + "author", + "community", + "community_author", + "community_follower", + "draft", + "draft_author", + "draft_topic", + "shout", + "shout_author", + "shout_topic", + "shout_reactions_followers", + "topic", + "topic_followers", + "reaction", + "invite", + "notification", + "collection", + "author_follower", + "author_rating", + "author_bookmark", + ] - create_table_if_not_exists(connection, model) # type: ignore[arg-type] - # logger.info(f"Created or verified table: {model.__tablename__}") - except Exception as e: - table_name = getattr(model, "__tablename__", str(model)) - logger.error(f"Error creating table {table_name}: {e}") - raise + registered_tables = list(Base.metadata.tables.keys()) + missing_tables = [table for table in required_tables if table not in registered_tables] + + if missing_tables: + logger.warning(f"Missing tables in Base.metadata: {missing_tables}") + logger.info(f"Available tables: {registered_tables}") + + # Создаем все таблицы стандартным способом SQLAlchemy + logger.info("Creating all database tables...") + Base.metadata.create_all(bind=engine) + + # Проверяем результат + from sqlalchemy import inspect + + inspector = inspect(engine) + created_tables = inspector.get_table_names() + logger.info(f"✅ Created tables: {created_tables}") + + # Проверяем критически важные таблицы + missing_created = [table for table in required_tables if table not in created_tables] + if missing_created: + error_msg = f"Failed to create critical tables: {missing_created}" + logger.error(f"❌ Missing critical tables: {missing_created}") + raise RuntimeError(error_msg) + + logger.info("✅ All critical tables created successfully") + + except Exception as e: + logger.error(f"❌ Error creating database tables: {e}") + raise diff --git a/tests/conftest.py b/tests/conftest.py index 455fd426..ce306a60 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -112,7 +112,8 @@ def ensure_all_models_imported(): # Проверяем что все модели зарегистрированы from orm.base import BaseModel as Base registered_tables = list(Base.metadata.tables.keys()) - print(f"🔍 All models imported, registered tables: {registered_tables}") + print(f"🔍 ensure_all_models_imported: {len(registered_tables)} tables registered") + print(f"📋 Registered tables: {registered_tables}") # Проверяем что все критические таблицы зарегистрированы required_tables = [ @@ -124,12 +125,15 @@ def ensure_all_models_imported(): ] missing_tables = [table for table in required_tables if table not in registered_tables] + if missing_tables: - print(f"⚠️ Missing tables in metadata: {missing_tables}") - print("🔄 Attempting to register missing models...") + print(f"⚠️ ensure_all_models_imported: missing tables: {missing_tables}") + print(f"Available tables: {registered_tables}") - # Пробуем импортировать модели явно + # Пробуем принудительно импортировать модели try: + print("🔄 ensure_all_models_imported: attempting explicit model imports...") + # Явно импортируем все модели from orm.community import Community, CommunityAuthor, CommunityFollower from orm.author import Author, AuthorFollower, AuthorRating, AuthorBookmark from orm.draft import Draft, DraftAuthor, DraftTopic @@ -144,17 +148,17 @@ def ensure_all_models_imported(): updated_tables = list(Base.metadata.tables.keys()) still_missing = [table for table in required_tables if table not in updated_tables] if still_missing: - print(f"⚠️ Still missing tables after explicit import: {still_missing}") + print(f"⚠️ ensure_all_models_imported: still missing tables: {still_missing}") else: - print("✅ All tables registered after explicit import") + print("✅ ensure_all_models_imported: all tables registered after explicit import") except Exception as e: - print(f"⚠️ Failed to import models explicitly: {e}") + print(f"⚠️ ensure_all_models_imported: failed to import models explicitly: {e}") else: - print("✅ All required tables registered in metadata") + print("✅ ensure_all_models_imported: all required tables registered in metadata") except Exception as e: - print(f"⚠️ Model import check failed: {e}") + print(f"⚠️ ensure_all_models_imported: model import check failed: {e}") # Проверяем импорт моделей ensure_all_models_imported()