Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion alembic.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[alembic]
script_location = alembic
script_location = zsim/api_src/services/database/migrations
sqlalchemy.url = sqlite:///zsim/data/zsim.db

[loggers]
Expand Down
75 changes: 0 additions & 75 deletions alembic/env.py

This file was deleted.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ exclude = [
"knowledge_base*",
]

[tool.setuptools.package-data]
"zsim" = ["api_src/services/database/migrations/**"]

[tool.pyinstaller]
# ZSim API 配置
spec_file = "zsim_api.spec"
Expand Down
8 changes: 8 additions & 0 deletions zsim/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from fastapi.middleware.cors import CORSMiddleware

from zsim.define import __version__
from zsim.api_src.services.database.migrate import run_migrations_to_head

dotenv.load_dotenv()

Expand All @@ -37,6 +38,13 @@
app.include_router(api_router, prefix="/api", tags=["ZSim API"])


@app.on_event("startup")
def apply_database_migrations() -> None:
"""在应用启动时自动执行数据库迁移。"""

run_migrations_to_head()


@app.get("/health")
async def health_check():
"""
Expand Down
35 changes: 35 additions & 0 deletions zsim/api_src/services/database/migrate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Utilities for programmatically running Alembic migrations."""

from __future__ import annotations

from contextlib import contextmanager

from alembic import command
from alembic.config import Config
from importlib.resources import as_file, files

from zsim.api_src.services.database.orm import get_sync_database_url, get_sync_engine


@contextmanager
def _alembic_cfg() -> Config:
"""构造指向包内迁移脚本的Alembic配置。"""

script_location = files("zsim.api_src.services.database.migrations")
with as_file(script_location) as script_dir:
cfg = Config()
cfg.set_main_option("script_location", str(script_dir))
cfg.set_main_option("sqlalchemy.url", get_sync_database_url())
yield cfg


def run_migrations_to_head() -> None:
"""确保数据库结构升级到最新版本。"""

# 初始化同步引擎以确保数据库文件和目录已创建
get_sync_engine()
with _alembic_cfg() as cfg:
command.upgrade(cfg, "head")


__all__ = ["run_migrations_to_head"]
File renamed without changes.
70 changes: 70 additions & 0 deletions zsim/api_src/services/database/migrations/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Alembic环境配置"""

from __future__ import annotations

import sys
from logging.config import fileConfig
from pathlib import Path

from alembic import context

PROJECT_ROOT = Path(__file__).resolve().parents[5]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
Comment on lines +11 to +13

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid hard-coded parent depth when loading packaged migrations

The new Alembic env now computes PROJECT_ROOT with Path(__file__).resolve().parents[5]. That works when the migrations live on disk at /…/zsim/api_src/services/database/migrations, but when run_migrations_to_head() loads the scripts via importlib.resources.as_file in a PyInstaller build the directory is extracted to a temporary folder such as /tmp/tmpabcd/migrations. In that context the parents sequence has fewer than six elements and this line raises IndexError before migrations run, causing the packaged API to crash during startup. Consider deriving the project root more defensively (e.g. walk up until you find the zsim package or guard against short paths) so the code works both in-source and when migrations are extracted to a flat temporary directory.

Useful? React with 👍 / 👎.


from zsim.api_src.services.database import ( # noqa: E402 # isort:skip
apl_db,
character_db,
enemy_db,
session_db,
)
from zsim.api_src.services.database.orm import ( # noqa: E402 # isort:skip
Base,
get_sync_database_url,
get_sync_engine,
)

config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)

target_metadata = Base.metadata
_ = (apl_db, character_db, enemy_db, session_db)


def run_migrations_offline() -> None:
"""Offline模式运行迁移"""

url = get_sync_database_url()
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
render_as_batch=True,
dialect_opts={"paramstyle": "named"},
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online() -> None:
"""Online模式运行迁移"""

connectable = get_sync_engine()

with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=True,
)

with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
File renamed without changes.
Empty file.
12 changes: 12 additions & 0 deletions zsim/api_src/services/database/orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from contextlib import asynccontextmanager
from pathlib import Path

from sqlalchemy import Engine, create_engine
from sqlalchemy.ext.asyncio import (
AsyncEngine,
AsyncSession,
Expand Down Expand Up @@ -55,6 +56,7 @@ def get_sync_database_url() -> str:

_async_engine: AsyncEngine = create_async_engine(get_async_database_url(), future=True)
_async_session_factory = async_sessionmaker(_async_engine, expire_on_commit=False)
_sync_engine: Engine | None = None


def get_async_engine() -> AsyncEngine:
Expand All @@ -67,6 +69,15 @@ def get_async_engine() -> AsyncEngine:
return _async_engine


def get_sync_engine() -> Engine:
"""返回复用的同步SQLAlchemy引擎实例。"""

global _sync_engine
if _sync_engine is None:
_sync_engine = create_engine(get_sync_database_url(), future=True)
return _sync_engine


@asynccontextmanager
async def get_async_session() -> AsyncIterator[AsyncSession]:
"""获取一个SQLAlchemy异步会话。
Expand Down Expand Up @@ -94,4 +105,5 @@ async def get_async_session() -> AsyncIterator[AsyncSession]:
"get_async_session",
"get_async_database_url",
"get_sync_database_url",
"get_sync_engine",
]
9 changes: 9 additions & 0 deletions zsim_api.spec
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ if config_json.exists():
datas.append((str(config_json), "zsim/config.json"))
print(f"Added configuration file: {config_json} -> zsim/config.json")

# Add packaged Alembic migrations so upgrades work after bundling
migrations_dir = project_root / "zsim" / "api_src" / "services" / "database" / "migrations"
if migrations_dir.exists():
datas.append((str(migrations_dir), "zsim/api_src/services/database/migrations"))
print(
"Added Alembic migrations directory: "
f"{migrations_dir} -> zsim/api_src/services/database/migrations"
)

# Add buff configuration file
buff_config_json = project_root / "zsim" / "sim_progress" / "Buff" / "buff_config.json"
if buff_config_json.exists():
Expand Down