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>
387 lines
12 KiB
Python
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()
|