[0.9.15] - 2025-08-30
All checks were successful
Deploy on push / deploy (push) Successful in 5m42s

### 🔧 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
This commit is contained in:
2025-08-30 22:20:58 +03:00
parent e1b0deeac0
commit 7325cdc5f5
5 changed files with 95 additions and 90 deletions

View File

@@ -1,5 +1,26 @@
# Changelog # 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 ## [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 - `settings` moved to base and now smaller
- new outside auth schema - new outside auth schema
- removed `gittask`, `auth`, `inbox`, `migration` - removed `gittask`, `auth`, `inbox`, `migration`

8
cache/precache.py vendored
View File

@@ -77,10 +77,6 @@ async def precache_topics_followers(topic_id: int, session) -> None:
followers_payload = fast_json_dumps(list(topic_followers)) followers_payload = fast_json_dumps(list(topic_followers))
await redis.execute("SET", f"topic:followers:{topic_id}", followers_payload) 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: except Exception as e:
logger.error(f"Error precaching followers for topic #{topic_id}: {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) topics_with_zero_followers.append(topic_slug)
if topics_with_zero_followers: if topics_with_zero_followers:
logger.info(f"📋 Топики с 0 фолловерами ({len(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}")
else: else:
logger.info("Все топики имеют фолловеров") logger.info("Все топики имеют фолловеров")

View File

@@ -9,28 +9,6 @@ import numpy as np
from settings import MUVERA_INDEX_NAME, SEARCH_MAX_BATCH_SIZE, SEARCH_PREFETCH_SIZE from settings import MUVERA_INDEX_NAME, SEARCH_MAX_BATCH_SIZE, SEARCH_PREFETCH_SIZE
from utils.logger import root_logger as logger 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 # Global collection for background tasks
background_tasks: List[asyncio.Task] = [] background_tasks: List[asyncio.Task] = []
@@ -303,7 +281,6 @@ class SearchService:
return return
# Запускаем метрики индексации # Запускаем метрики индексации
search_metrics.start_indexing(len(shouts))
start_time = time.time() start_time = time.time()
logger.info(f"Starting Muvera bulk indexing of {len(shouts)} documents") 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"Muvera bulk indexing completed in {elapsed:.2f}s: "
f"{len(documents)} documents indexed, {total_skipped} shouts skipped" f"{len(documents)} documents indexed, {total_skipped} shouts skipped"
) )
# Завершаем метрики индексации
search_metrics.end_indexing()
except Exception as e: except Exception as e:
logger.exception(f"Muvera bulk indexing failed: {e}") logger.exception(f"Muvera bulk indexing failed: {e}")
else: else:

View File

@@ -9,9 +9,7 @@ from ariadne import (
load_schema_from_path, load_schema_from_path,
) )
from orm import collection, community, draft, invite, notification, reaction, shout, topic from storage.db import engine
from orm.author import Author, AuthorBookmark, AuthorFollower, AuthorRating
from storage.db import create_table_if_not_exists
# Создаем основные типы # Создаем основные типы
query = QueryType() query = QueryType()
@@ -36,52 +34,64 @@ resolvers: SchemaBindable | type[Enum] | list[SchemaBindable | type[Enum]] = [
def create_all_tables() -> None: def create_all_tables() -> None:
"""Create all database tables in the correct order.""" """Create all database tables using SQLAlchemy's standard approach."""
# Порядок важен - сначала таблицы без внешних ключей, затем зависимые таблицы try:
models_in_order = [ # Импортируем все модели чтобы они были зарегистрированы в Base.metadata
# 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
]
from storage.db import engine # Получаем Base с зарегистрированными моделями
from orm.base import BaseModel as Base
# Используем одно соединение для всех таблиц, чтобы избежать проблем с транзакциями # Проверяем что все критические таблицы зарегистрированы
with engine.connect() as connection: required_tables = [
for model in models_in_order: "author",
try: "community",
# Ensure model is a type[DeclarativeBase] "community_author",
if not hasattr(model, "__tablename__"): "community_follower",
logger.warning(f"Skipping {model} - not a DeclarativeBase model") "draft",
continue "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] registered_tables = list(Base.metadata.tables.keys())
# logger.info(f"Created or verified table: {model.__tablename__}") missing_tables = [table for table in required_tables if table not in registered_tables]
except Exception as e:
table_name = getattr(model, "__tablename__", str(model)) if missing_tables:
logger.error(f"Error creating table {table_name}: {e}") logger.warning(f"Missing tables in Base.metadata: {missing_tables}")
raise 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

View File

@@ -112,7 +112,8 @@ def ensure_all_models_imported():
# Проверяем что все модели зарегистрированы # Проверяем что все модели зарегистрированы
from orm.base import BaseModel as Base from orm.base import BaseModel as Base
registered_tables = list(Base.metadata.tables.keys()) 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 = [ 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] missing_tables = [table for table in required_tables if table not in registered_tables]
if missing_tables: if missing_tables:
print(f"⚠️ Missing tables in metadata: {missing_tables}") print(f"⚠️ ensure_all_models_imported: missing tables: {missing_tables}")
print("🔄 Attempting to register missing models...") print(f"Available tables: {registered_tables}")
# Пробуем импортировать модели явно # Пробуем принудительно импортировать модели
try: try:
print("🔄 ensure_all_models_imported: attempting explicit model imports...")
# Явно импортируем все модели
from orm.community import Community, CommunityAuthor, CommunityFollower from orm.community import Community, CommunityAuthor, CommunityFollower
from orm.author import Author, AuthorFollower, AuthorRating, AuthorBookmark from orm.author import Author, AuthorFollower, AuthorRating, AuthorBookmark
from orm.draft import Draft, DraftAuthor, DraftTopic from orm.draft import Draft, DraftAuthor, DraftTopic
@@ -144,17 +148,17 @@ def ensure_all_models_imported():
updated_tables = list(Base.metadata.tables.keys()) updated_tables = list(Base.metadata.tables.keys())
still_missing = [table for table in required_tables if table not in updated_tables] still_missing = [table for table in required_tables if table not in updated_tables]
if still_missing: 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: else:
print("All tables registered after explicit import") print("ensure_all_models_imported: all tables registered after explicit import")
except Exception as e: 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: else:
print("All required tables registered in metadata") print("ensure_all_models_imported: all required tables registered in metadata")
except Exception as e: 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() ensure_all_models_imported()