### 🔧 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:
22
CHANGELOG.md
22
CHANGELOG.md
@@ -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
8
cache/precache.py
vendored
@@ -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("✅ Все топики имеют фолловеров")
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user