Files
orion/alembic/env.py
Samir Boulahtit c3d26e9aa4 refactor(migrations): squash 75 migrations into 12 per-module initial migrations
The old migration chain was broken (downgrade path through vendor->merchant
rename made rollbacks impossible). This squashes everything into fresh
per-module migrations with zero schema drift, verified by autogenerate.

Changes:
- Replace 75 accumulated migrations with 12 per-module initial migrations
  (core, billing, catalog, marketplace, cms, customers, orders, inventory,
  cart, messaging, loyalty, dev_tools) in a linear chain
- Fix make db-reset to use SQL DROP SCHEMA instead of alembic downgrade base
- Enable migration autodiscovery for all modules (migrations_path in definitions)
- Rewrite alembic/env.py to import all 75 model tables across 13 modules
- Fix AdminNotification import (was incorrectly from tenancy, now from messaging)
- Update squash_migrations.py to handle all module migration directories

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 11:51:37 +01:00

387 lines
12 KiB
Python

# alembic/env.py
"""
Alembic migration environment configuration.
This file is responsible for:
1. Importing ALL database models so Alembic can detect schema changes
2. Configuring the database connection
3. Running migrations in online/offline mode
CRITICAL: Every model in models/database/__init__.py must be imported here!
"""
import os
import sys
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context
# Add your project directory to the Python path
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from app.core.config import settings
from models.database.base import Base
# ============================================================================
# IMPORT ALL DATABASE MODELS
# ============================================================================
# CRITICAL: Every model must be imported here so Alembic can detect tables!
# If a model is not imported, Alembic will not create/update its table.
# ============================================================================
print("[ALEMBIC] Importing database models...")
print("=" * 70)
_import_errors = []
# ----------------------------------------------------------------------------
# CORE MODULE (1 model)
# ----------------------------------------------------------------------------
try:
from app.modules.core.models import AdminMenuConfig # noqa: F401
print(" ✓ Core models (1)")
except ImportError as e:
_import_errors.append(f"core: {e}")
print(f" ✗ Core models failed: {e}")
# ----------------------------------------------------------------------------
# TENANCY MODULE (15 models)
# ----------------------------------------------------------------------------
try:
from app.modules.tenancy.models import ( # noqa: F401
AdminAuditLog,
AdminPlatform,
AdminSession,
AdminSetting,
ApplicationLog,
Merchant,
Platform,
PlatformAlert,
PlatformModule,
Role,
Store,
StoreDomain,
StorePlatform,
StoreUser,
User,
)
print(" ✓ Tenancy models (15)")
except ImportError as e:
_import_errors.append(f"tenancy: {e}")
print(f" ✗ Tenancy models failed: {e}")
# ----------------------------------------------------------------------------
# BILLING MODULE (9 models)
# ----------------------------------------------------------------------------
try:
from app.modules.billing.models import ( # noqa: F401
AddOnProduct,
BillingHistory,
CapacitySnapshot,
MerchantFeatureOverride,
MerchantSubscription,
StoreAddOn,
StripeWebhookEvent,
SubscriptionTier,
TierFeatureLimit,
)
print(" ✓ Billing models (9)")
except ImportError as e:
_import_errors.append(f"billing: {e}")
print(f" ✗ Billing models failed: {e}")
# ----------------------------------------------------------------------------
# CATALOG MODULE (3 models)
# ----------------------------------------------------------------------------
try:
from app.modules.catalog.models import ( # noqa: F401
Product,
ProductMedia,
ProductTranslation,
)
print(" ✓ Catalog models (3)")
except ImportError as e:
_import_errors.append(f"catalog: {e}")
print(f" ✗ Catalog models failed: {e}")
# ----------------------------------------------------------------------------
# MARKETPLACE MODULE (10 models)
# ----------------------------------------------------------------------------
try:
from app.modules.marketplace.models import ( # noqa: F401
LetzshopFulfillmentQueue,
LetzshopHistoricalImportJob,
LetzshopStoreCache,
LetzshopSyncLog,
MarketplaceImportError,
MarketplaceImportJob,
MarketplaceProduct,
MarketplaceProductTranslation,
StoreLetzshopCredentials,
StoreOnboarding,
)
print(" ✓ Marketplace models (10)")
except ImportError as e:
_import_errors.append(f"marketplace: {e}")
print(f" ✗ Marketplace models failed: {e}")
# ----------------------------------------------------------------------------
# CMS MODULE (3 models)
# ----------------------------------------------------------------------------
try:
from app.modules.cms.models import ( # noqa: F401
ContentPage,
MediaFile,
StoreTheme,
)
print(" ✓ CMS models (3)")
except ImportError as e:
_import_errors.append(f"cms: {e}")
print(f" ✗ CMS models failed: {e}")
# ----------------------------------------------------------------------------
# CUSTOMERS MODULE (3 models)
# ----------------------------------------------------------------------------
try:
from app.modules.customers.models import ( # noqa: F401
Customer,
CustomerAddress,
PasswordResetToken,
)
print(" ✓ Customers models (3)")
except ImportError as e:
_import_errors.append(f"customers: {e}")
print(f" ✗ Customers models failed: {e}")
# ----------------------------------------------------------------------------
# ORDERS MODULE (5 models)
# ----------------------------------------------------------------------------
try:
from app.modules.orders.models import ( # noqa: F401
Invoice,
Order,
OrderItem,
OrderItemException,
StoreInvoiceSettings,
)
print(" ✓ Orders models (5)")
except ImportError as e:
_import_errors.append(f"orders: {e}")
print(f" ✗ Orders models failed: {e}")
# ----------------------------------------------------------------------------
# INVENTORY MODULE (2 models)
# ----------------------------------------------------------------------------
try:
from app.modules.inventory.models import ( # noqa: F401
Inventory,
InventoryTransaction,
)
print(" ✓ Inventory models (2)")
except ImportError as e:
_import_errors.append(f"inventory: {e}")
print(f" ✗ Inventory models failed: {e}")
# ----------------------------------------------------------------------------
# CART MODULE (1 model)
# ----------------------------------------------------------------------------
try:
from app.modules.cart.models import CartItem # noqa: F401
print(" ✓ Cart models (1)")
except ImportError as e:
_import_errors.append(f"cart: {e}")
print(f" ✗ Cart models failed: {e}")
# ----------------------------------------------------------------------------
# MESSAGING MODULE (9 models)
# ----------------------------------------------------------------------------
try:
from app.modules.messaging.models import ( # noqa: F401
AdminNotification,
Conversation,
ConversationParticipant,
EmailLog,
EmailTemplate,
Message,
MessageAttachment,
StoreEmailSettings,
StoreEmailTemplate,
)
print(" ✓ Messaging models (9)")
except ImportError as e:
_import_errors.append(f"messaging: {e}")
print(f" ✗ Messaging models failed: {e}")
# ----------------------------------------------------------------------------
# LOYALTY MODULE (6 models)
# ----------------------------------------------------------------------------
try:
from app.modules.loyalty.models import ( # noqa: F401
AppleDeviceRegistration,
LoyaltyCard,
LoyaltyProgram,
LoyaltyTransaction,
MerchantLoyaltySettings,
StaffPin,
)
print(" ✓ Loyalty models (6)")
except ImportError as e:
_import_errors.append(f"loyalty: {e}")
print(f" ✗ Loyalty models failed: {e}")
# ----------------------------------------------------------------------------
# DEV_TOOLS MODULE (8 models)
# ----------------------------------------------------------------------------
try:
from app.modules.dev_tools.models import ( # noqa: F401
ArchitectureRule,
ArchitectureScan,
ArchitectureViolation,
TestCollection,
TestResult,
TestRun,
ViolationAssignment,
ViolationComment,
)
print(" ✓ Dev Tools models (8)")
except ImportError as e:
_import_errors.append(f"dev_tools: {e}")
print(f" ✗ Dev Tools models failed: {e}")
# ============================================================================
# SUMMARY
# ============================================================================
print("=" * 70)
if _import_errors:
print(f"[ALEMBIC] WARNING: {len(_import_errors)} import error(s):")
for err in _import_errors:
print(f" - {err}")
print("=" * 70)
print(f"[ALEMBIC] Total tables in metadata: {len(Base.metadata.tables)}")
print("=" * 70)
print()
# ============================================================================
# ALEMBIC CONFIGURATION
# ============================================================================
# Alembic Config object
config = context.config
# Override sqlalchemy.url with our settings
config.set_main_option("sqlalchemy.url", settings.database_url)
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Set target metadata from Base
target_metadata = Base.metadata
# ============================================================================
# MODULE MIGRATIONS DISCOVERY
# ============================================================================
# Discover migration paths from self-contained modules.
# Each module can have its own migrations/ directory.
# ============================================================================
def get_version_locations() -> list[str]:
"""
Get all version locations including module migrations.
Returns:
List of paths to migration version directories
"""
try:
from app.modules.migrations import get_all_migration_paths
paths = get_all_migration_paths()
locations = [str(p) for p in paths if p.exists()]
if len(locations) > 1:
print(f"[ALEMBIC] Found {len(locations)} migration locations:")
for loc in locations:
print(f" - {loc}")
return locations
except ImportError:
# Fallback if module migrations not available
return ["alembic/versions"]
# Get version locations for multi-directory support
version_locations = get_version_locations()
def run_migrations_offline() -> None:
"""
Run migrations in 'offline' mode.
This configures the context with just a URL and not an Engine,
though an Engine is acceptable here as well. By skipping the Engine
creation we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
version_locations=version_locations,
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""
Run migrations in 'online' mode.
In this scenario we need to create an Engine and associate a connection
with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
version_locations=version_locations,
)
with context.begin_transaction():
context.run_migrations()
# ============================================================================
# MAIN EXECUTION
# ============================================================================
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()