Introduce Alembic at v1. Migrations live inside the package (decnet/web/db/migrations) so they ship with installs; alembic.ini at the repo root drives the CLI. env.py is async and dual-backend, selecting the engine from DECNET_DB_TYPE (mirroring db/factory.py) and reusing the app's own connection when run programmatically. The baseline captures all 39 tables. _BIG_TEXT round-trips as Text().with_variant(MEDIUMTEXT, 'mysql'), so both backends get the right column type from the migration. kd_digraph_simhash gains a sqlite BLOB variant: BINARY(8) reflects as NUMERIC on SQLite and would otherwise trip 'alembic check' forever.
91 lines
3.1 KiB
Python
91 lines
3.1 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""Alembic environment — async, dual-backend (sqlite | mysql).
|
|
|
|
Two entry shapes:
|
|
|
|
* **Programmatic** (app boot): :func:`decnet.web.db.migrate.run_migrations`
|
|
passes the app's own sync ``Connection`` via ``config.attributes`` so the
|
|
upgrade rides the existing engine — no second connection, no extra driver.
|
|
* **Standalone** (``alembic`` CLI: autogenerate, upgrade, history): builds its
|
|
own async engine from ``DECNET_DB_TYPE``, mirroring ``db/factory.py``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
from logging.config import fileConfig
|
|
|
|
from alembic import context
|
|
from sqlalchemy.engine import Connection
|
|
from sqlmodel import SQLModel
|
|
|
|
# Importing the models package registers every table on SQLModel.metadata,
|
|
# which is what autogenerate diffs against.
|
|
import decnet.web.db.models # noqa: F401
|
|
|
|
config = context.config
|
|
|
|
# Standalone CLI runs configure logging from alembic.ini; the programmatic
|
|
# path builds a Config with no file, so guard on it.
|
|
if config.config_file_name is not None:
|
|
fileConfig(config.config_file_name)
|
|
|
|
target_metadata = SQLModel.metadata
|
|
|
|
|
|
def _build_async_engine():
|
|
"""Standalone-only: pick an async engine the way db/factory.py does."""
|
|
db_type = os.environ.get("DECNET_DB_TYPE", "sqlite").lower()
|
|
if db_type == "sqlite":
|
|
from decnet.config import _ROOT
|
|
from decnet.web.db.sqlite.database import get_async_engine as sqlite_engine
|
|
db_path = os.environ.get("DECNET_DB_PATH", str(_ROOT / "decnet.db"))
|
|
return sqlite_engine(db_path)
|
|
if db_type == "mysql":
|
|
from decnet.web.db.mysql.database import get_async_engine as mysql_engine
|
|
return mysql_engine()
|
|
raise ValueError(f"Unsupported database type: {db_type}")
|
|
|
|
|
|
def _configure_and_run(connection: Connection) -> None:
|
|
context.configure(
|
|
connection=connection,
|
|
target_metadata=target_metadata,
|
|
# SQLite can't ALTER in place; batch mode rewrites the table so future
|
|
# migrations (drop/alter column) work on both backends.
|
|
render_as_batch=connection.dialect.name == "sqlite",
|
|
compare_type=True,
|
|
)
|
|
with context.begin_transaction():
|
|
context.run_migrations()
|
|
|
|
|
|
async def _run_standalone() -> None:
|
|
engine = _build_async_engine()
|
|
async with engine.connect() as connection:
|
|
await connection.run_sync(_configure_and_run)
|
|
await engine.dispose()
|
|
|
|
|
|
def run_migrations_online() -> None:
|
|
connection = config.attributes.get("connection", None)
|
|
if connection is not None:
|
|
# Programmatic: app handed us a live sync Connection (via run_sync).
|
|
_configure_and_run(connection)
|
|
else:
|
|
asyncio.run(_run_standalone())
|
|
|
|
|
|
if context.is_offline_mode():
|
|
# Offline (--sql) mode: emit DDL without a DB. Cheap to support and keeps
|
|
# `alembic upgrade head --sql` working for operators who want to review SQL.
|
|
context.configure(
|
|
url=os.environ.get("DECNET_DB_URL"),
|
|
target_metadata=target_metadata,
|
|
literal_binds=True,
|
|
)
|
|
with context.begin_transaction():
|
|
context.run_migrations()
|
|
else:
|
|
run_migrations_online()
|