Move migration logic to alembic env.py and add CLI wrappers

MA
18m ago
+1068-844

Type of change

  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Description

This PR refactors the database migration system to move logic out of reflex.model into a generated alembic/env.py template, and provides thin CLI wrappers in reflex.utils.migrations for the reflex db commands.

Key changes:

  1. New reflex/.templates/alembic/env.py: A template that reflex db init installs, which:

    • Loads the app to register all models
    • Reads db_url from rxconfig.py instead of alembic.ini
    • Handles column defaults (callable and scalar) via a rewriter
    • Skips writing empty autogenerated revisions
    • Imports sqlmodel when needed
  2. New reflex/utils/migrations.py: Thin wrappers over alembic's CLI:

    • run(): Execute alembic commands with proper error handling
    • init(): Run alembic init and install the reflex template
    • script_directory(): Locate the alembic scripts directory
    • is_legacy_env(): Detect stock alembic templates from older releases
    • update_env(): Replace legacy env.py with the reflex template
    • describe_error(): Format alembic errors for display
  3. Refactored reflex/reflex.py (db_cli commands):

    • db init: Now calls migrations.init() and applies the initial revision
    • db migrate: Calls migrations.run("upgrade", "head")
    • db makemigrations: Calls migrations.run("revision", "--autogenerate", ...)
    • db status: Calls migrations.run("history", ...)
    • Added _alembic_errors() context manager to catch and report CommandError
    • Added logic to detect and update legacy migration environments
  4. Removed from reflex/model.py:

    • alembic_autogenerate(): Logic moved to env.py's rewriter
    • get_migration_history(): No longer needed; alembic history handles it
    • format_revision(): Moved to reflex.py for db status
    • _alembic_config(), _alembic_render_item(), _alembic_upgrade(): Integrated into env.py
    • Simplified alembic_init() to just call migrations.init()
    • Simplified migrate() to call migrations.run()
  5. Updated reflex/utils/prerequisites.py:

    • check_db_initialized(): Now also checks for legacy env.py
    • check_schema_up_to_date(): Refactored to use alembic's native commands
  6. Comprehensive test suite in tests/units/utils/test_migrations.py:

    • Tests for both SQLModel and SQLAlchemy ORMs
    • Tests for schema evolution (add/drop columns, add tables)
    • Tests for callable defaults being evaluated for existing rows
    • Tests for UTC datetime migrations
    • Tests for legacy environment detection and update
    • Tests for error reporting and initialization checks

Breaking Changes

The following functions are removed from the public API:

  • reflex.model.alembic_autogenerate()
  • reflex.model.get_migration_history()
  • reflex.model.format_revision()

These were internal implementation details; the reflex db CLI remains the public interface.

Test Plan

  • Added 511 lines of comprehensive tests in tests/units/utils/test_migrations.py covering:
    • Both SQLModel and SQLAlchemy ORMs
    • Schema evolution scenarios (add/drop columns and tables)
    • Callable defaults evaluation