xml-pipeline/bloxserver/alembic/env.py
Donna a6f44b1fc9 Set up Alembic database migrations
- Initialize Alembic in bloxserver/
- Configure for async SQLAlchemy (converts async URLs to sync for migrations)
- Generate initial migration with all existing tables:
  - users (Clerk sync, Stripe billing)
  - flows (workflow definitions)
  - triggers (webhook, schedule, manual, event)
  - executions (run history)
  - user_api_keys (BYOK encrypted storage)
  - usage_records (billing metrics)
  - stripe_events (webhook idempotency)

Usage:
  cd bloxserver
  alembic upgrade head    # Apply migrations
  alembic revision --autogenerate -m 'description'  # New migration

Co-authored-by: Dan
2026-01-26 07:21:56 +00:00

108 lines
2.8 KiB
Python

"""
Alembic migration environment.
Configured for async SQLAlchemy with PostgreSQL/SQLite.
"""
from __future__ import annotations
import os
import sys
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context
# Add parent directory to path so 'bloxserver' package is importable
# bloxserver/ is at xml-pipeline/bloxserver/, so we need xml-pipeline/ on the path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
# Import models to register them with Base.metadata
from bloxserver.api.models.database import Base
from bloxserver.api.models import tables # noqa: F401 - imports register models
# Alembic Config object
config = context.config
# Setup logging from alembic.ini
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Target metadata for autogenerate
target_metadata = Base.metadata
def get_url() -> str:
"""
Get database URL from environment.
Converts async URLs (postgresql+asyncpg://) to sync (postgresql://)
because Alembic runs migrations synchronously.
"""
url = os.getenv(
"DATABASE_URL",
"sqlite:///./bloxserver.db",
)
# Convert async driver to sync for Alembic
if url.startswith("postgresql+asyncpg://"):
url = url.replace("postgresql+asyncpg://", "postgresql://", 1)
elif url.startswith("sqlite+aiosqlite://"):
url = url.replace("sqlite+aiosqlite://", "sqlite://", 1)
return url
def run_migrations_offline() -> None:
"""
Run migrations in 'offline' mode.
Generates SQL script without connecting to database.
Useful for reviewing migrations before applying.
"""
url = get_url()
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
compare_server_default=True,
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""
Run migrations in 'online' mode.
Connects to database and applies migrations directly.
"""
configuration = config.get_section(config.config_ini_section, {})
configuration["sqlalchemy.url"] = get_url()
connectable = engine_from_config(
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
compare_server_default=True,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()