Files
2026-07-13 11:59:58 +08:00

79 lines
2.9 KiB
Python

"""Generate a new alembic revision against an ephemeral SQLite DB.
Used by ``make migrate-rev MSG="..."``. Avoids two pitfalls:
1. ``alembic.ini``'s default ``sqlalchemy.url`` (``sqlite:///./data/deerflow.db``)
points at a path that doesn't exist in a clean checkout, so a bare
``alembic revision --autogenerate`` fails with ``unable to open database file``.
2. A persistent DB might be at an unknown revision (or at no revision at all),
producing a noisy autogenerate diff that mixes "real" changes with
accidentally-detected drift.
This script builds a *fresh* temp SQLite, runs the existing alembic chain to
``head`` against it, then runs ``alembic revision --autogenerate`` against
that. The temp DB must be built from migration history -- not from
``Base.metadata.create_all`` -- so newly edited ORM fields that do not yet have
a revision remain visible to autogenerate as a real diff.
The generated file lands in
``packages/harness/deerflow/persistence/migrations/versions/`` -- exactly
where alembic puts it by default -- and the temp directory is left for the OS
to GC. Review the generated revision and switch raw ``op.add_column`` /
``op.drop_column`` calls to the idempotent helpers in ``migrations/_helpers.py``
before committing.
Run from the ``backend/`` directory:
PYTHONPATH=. uv run python scripts/_autogen_revision.py "MESSAGE"
or via Makefile:
make migrate-rev MSG="..."
"""
from __future__ import annotations
import os
import sys
import tempfile
from pathlib import Path
from alembic import command
from alembic.config import Config
import deerflow.persistence.models # noqa: F401 -- registers ORM models with Base.metadata
from deerflow.persistence.bootstrap import _escape_url_for_alembic
BACKEND_DIR = Path(__file__).resolve().parents[1]
MIGRATIONS_DIR = BACKEND_DIR / "packages/harness/deerflow/persistence/migrations"
def _alembic_config(url: str) -> Config:
cfg = Config()
cfg.set_main_option("script_location", str(MIGRATIONS_DIR))
# Shared with ``bootstrap._alembic_safe_url`` so the ConfigParser ``%``
# interpolation rule lives in one place.
cfg.set_main_option("sqlalchemy.url", _escape_url_for_alembic(url))
return cfg
def _build_temp_db_at_head() -> str:
tmpdir = tempfile.mkdtemp(prefix="deerflow-autogen-")
db_path = os.path.join(tmpdir, "autogen.db").replace(os.sep, "/")
url = f"sqlite+aiosqlite:///{db_path}"
command.upgrade(_alembic_config(url), "head")
return url
def main() -> None:
if len(sys.argv) < 2 or not sys.argv[1].strip():
print('usage: python scripts/_autogen_revision.py "describe the change"', file=sys.stderr)
sys.exit(2)
message = sys.argv[1]
url = _build_temp_db_at_head()
print(f"autogen: built temp DB at head: {url}", file=sys.stderr)
command.revision(_alembic_config(url), message=message, autogenerate=True)
if __name__ == "__main__":
main()