Spaces:
Running
Running
| """core/startup/db.py β Database initialization on startup. | |
| Runs on application startup (called from lifespan). | |
| Uses safe ALTER TABLE ADD COLUMN (errors swallowed = column already exists). | |
| New columns MUST be added here when added to any model. | |
| """ | |
| import structlog | |
| from sqlalchemy import text | |
| logger = structlog.get_logger(__name__) | |
| # ββ All incremental column migrations ββββββββββββββββββββββββββββββββββββββββ | |
| # Format: (table, column, sqlite_type) | |
| # Safe: if column already exists the ALTER TABLE fails silently. | |
| _COLUMN_MIGRATIONS: list[tuple[str, str, str]] = [ | |
| # book_platform_listings (original migrations) | |
| ("book_platform_listings", "list_price", "VARCHAR(32)"), | |
| ("book_platform_listings", "price_currency", "VARCHAR(8)"), | |
| # users β Phase 1 billing fields | |
| ("users", "stripe_customer_id", "VARCHAR(255)"), | |
| ("users", "stripe_subscription_id", "VARCHAR(255)"), | |
| ("users", "stripe_plan_id", "VARCHAR(100)"), | |
| ("users", "payment_grace_until", "DATETIME"), | |
| ("users", "remove_branding", "BOOLEAN DEFAULT 0"), | |
| ("users", "custom_widget_css", "TEXT"), | |
| # users β Phase 3 white-label | |
| ("users", "custom_domain", "VARCHAR(255)"), | |
| ("users", "domain_verified", "BOOLEAN DEFAULT 0"), | |
| # users β Phase 4 agency / SSO / GDPR | |
| ("users", "stripe_connect_account_id", "VARCHAR(255)"), | |
| ("users", "notes", "TEXT"), | |
| # referral_links (Phase 3) | |
| ("referral_links", "stripe_coupon_id", "VARCHAR(255)"), | |
| ("referral_links", "clicks", "INTEGER DEFAULT 0"), | |
| ("referral_links", "signups", "INTEGER DEFAULT 0"), | |
| ("referral_links", "is_active", "BOOLEAN DEFAULT 1"), | |
| # referral_commissions (Phase 3) | |
| ("referral_commissions", "paid_at", "DATETIME"), | |
| # client_access β SuperAdmin grants (notes column used by grants.py) | |
| ("client_access", "notes", "TEXT"), | |
| ("client_access", "auto_renew", "BOOLEAN DEFAULT 0"), | |
| ("client_access", "bonus_tokens", "INTEGER DEFAULT 0"), | |
| ("client_access", "plan_id", "VARCHAR(50)"), | |
| ("client_access", "stripe_subscription_id", "VARCHAR(255)"), | |
| ] | |
| async def init_db() -> None: | |
| """Create all tables then apply incremental column migrations. | |
| Safe to run on every startup: | |
| - create_all is idempotent (skips existing tables) | |
| - ALTER TABLE errors are swallowed (column already exists) | |
| """ | |
| from app.models.base import Base | |
| from app.dependencies import get_engine | |
| engine = get_engine() | |
| async with engine.begin() as conn: | |
| # Create any brand-new tables (new models from this deploy) | |
| await conn.run_sync(Base.metadata.create_all) | |
| # Add new columns to existing tables | |
| await _run_column_migrations(conn) | |
| logger.info("Database tables ready") | |
| async def _run_column_migrations(conn) -> None: | |
| """Execute ALTER TABLE ADD COLUMN statements; silently skip if column exists.""" | |
| added, skipped = 0, 0 | |
| for table, column, col_type in _COLUMN_MIGRATIONS: | |
| try: | |
| await conn.execute( | |
| text(f"ALTER TABLE {table} ADD COLUMN {column} {col_type}") | |
| ) | |
| added += 1 | |
| except Exception: | |
| skipped += 1 # column already exists β normal on every restart after first | |
| if added: | |
| logger.info("Column migrations applied", added=added, skipped=skipped) | |