diff --git a/alembic.ini b/alembic.ini index 90fdbe2d..eced32b8 100644 --- a/alembic.ini +++ b/alembic.ini @@ -1,5 +1,5 @@ [alembic] -script_location = alembic +script_location = zsim/api_src/services/database/migrations sqlalchemy.url = sqlite:///zsim/data/zsim.db [loggers] diff --git a/alembic/env.py b/alembic/env.py deleted file mode 100644 index 40bdf971..00000000 --- a/alembic/env.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Alembic环境配置""" - -from __future__ import annotations - -import sys -from logging.config import fileConfig -from pathlib import Path - -from sqlalchemy import engine_from_config, pool - -from alembic import context - -PROJECT_ROOT = Path(__file__).resolve().parents[1] -if str(PROJECT_ROOT) not in sys.path: - sys.path.insert(0, str(PROJECT_ROOT)) - -config = context.config -if config.config_file_name is not None: - fileConfig(config.config_file_name) - - -def _load_metadata(): - """加载SQLAlchemy元数据""" - - import zsim.api_src.services.database.apl_db # noqa: F401 - import zsim.api_src.services.database.character_db # noqa: F401 - import zsim.api_src.services.database.enemy_db # noqa: F401 - import zsim.api_src.services.database.session_db # noqa: F401 - from zsim.api_src.services.database.orm import Base - - return Base.metadata - - -def _get_database_url() -> str: - """获取同步数据库URL""" - - from zsim.api_src.services.database.orm import get_sync_database_url - - return get_sync_database_url() - - -target_metadata = _load_metadata() -config.set_main_option("sqlalchemy.url", _get_database_url()) - - -def run_migrations_offline() -> None: - """Offline模式运行迁移""" - - url = config.get_main_option("sqlalchemy.url") - context.configure(url=url, target_metadata=target_metadata, literal_binds=True) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online() -> None: - """Online模式运行迁移""" - - connectable = engine_from_config( - config.get_section(config.config_ini_section) or {}, - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure(connection=connection, target_metadata=target_metadata) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/pyproject.toml b/pyproject.toml index a56a6a7c..dd2a6de6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/zsim/api.py b/zsim/api.py index 65775209..831d2c6c 100644 --- a/zsim/api.py +++ b/zsim/api.py @@ -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() @@ -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(): """ diff --git a/zsim/api_src/services/database/migrate.py b/zsim/api_src/services/database/migrate.py new file mode 100644 index 00000000..a039c359 --- /dev/null +++ b/zsim/api_src/services/database/migrate.py @@ -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"] diff --git a/alembic/versions/.gitkeep b/zsim/api_src/services/database/migrations/__init__.py similarity index 100% rename from alembic/versions/.gitkeep rename to zsim/api_src/services/database/migrations/__init__.py diff --git a/zsim/api_src/services/database/migrations/env.py b/zsim/api_src/services/database/migrations/env.py new file mode 100644 index 00000000..b01ffc81 --- /dev/null +++ b/zsim/api_src/services/database/migrations/env.py @@ -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)) + +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() diff --git a/alembic/script.py.mako b/zsim/api_src/services/database/migrations/script.py.mako similarity index 100% rename from alembic/script.py.mako rename to zsim/api_src/services/database/migrations/script.py.mako diff --git a/zsim/api_src/services/database/migrations/versions/.gitkeep b/zsim/api_src/services/database/migrations/versions/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/alembic/versions/74ee1818bd42_init_schema.py b/zsim/api_src/services/database/migrations/versions/74ee1818bd42_init_schema.py similarity index 100% rename from alembic/versions/74ee1818bd42_init_schema.py rename to zsim/api_src/services/database/migrations/versions/74ee1818bd42_init_schema.py diff --git a/zsim/api_src/services/database/orm.py b/zsim/api_src/services/database/orm.py index c958500f..ade96c2d 100644 --- a/zsim/api_src/services/database/orm.py +++ b/zsim/api_src/services/database/orm.py @@ -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, @@ -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: @@ -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异步会话。 @@ -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", ] diff --git a/zsim_api.spec b/zsim_api.spec index 2f2e35d8..74c10a11 100644 --- a/zsim_api.spec +++ b/zsim_api.spec @@ -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():