#!/usr/bin/env python3 # /home/Autopartes/pos/migrations/runner.py """Apply schema migrations to all tenant databases.""" import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from tenant_db import get_master_conn, get_tenant_conn_by_dbname MIGRATIONS_DIR = os.path.dirname(os.path.abspath(__file__)) # Migration registry: version -> filename MIGRATIONS = { "v1.0": "v1.0_initial.sql", "v1.1": "v1.1_pos_tables.sql", "v1.2": "v1.2_subdomain.sql", "v1.3": "v1.3_fleet.sql", "v1.4": "v1.4_whatsapp.sql", "v1.5": "v1.5_returns.sql", "v1.6": "v1.6_marketplace.sql", "v1.7": "v1.7_plates.sql", "v1.8": "v1.8_performance_indexes.sql", "v1.9": "v1.9_redis_cache.sql", "v2.0": "v2.0_multi_currency.sql", "v2.1": "v2.1_suppliers.sql", "v2.2": "v2.2_alerts_warranty.sql", "v2.3": "v2.3_metabase.sql", "v2.4": "v2.4_crm_enhanced.sql", "v2.5": "v2.5_service_orders.sql", "v2.6": "v2.6_bnpl_erp.sql", "v2.7": "v2.7_notifications.sql", "v2.8": "v2.8_savings.sql", "v2.9": "v2.9_logistics.sql", "v3.0": "v3.0_public_api.sql", "v3.1": "v3.1_inventory_vehicle_compat.sql", "v3.2": "v3.2_db_performance.sql", "v3.2.1": "v3.2_qwen_vehicle_compat.sql", "v3.3": "v3.3_marketplace_any_part.sql", "v3.3.1": "v3.3_materialized_view.sql", "v3.4": "v3.4_meli_integration.sql", "v3.5": "v3.5_meli_questions.sql", "v3.5.1": "v3.5_whatsapp_state_machine.sql", "v3.6": "v3.6_dropshipping.sql", "v3.7": "v3.7_sku_aliases.sql", "v3.8": "v3.8_supplier_catalog.sql", "v3.9": "v3.9_supplier_catalog_prices.sql", "v4.0": "v4.0_multi_branch.sql", "v4.1": "v4.1_global_invoice.sql", "v4.2": "v4.2_meli_sync_queue.sql", "v4.3": "v4.3_facturapi.sql", } def get_all_tenants(): """Get all tenants with their current schema version.""" conn = get_master_conn() cur = conn.cursor() cur.execute(""" SELECT t.id, t.db_name, t.name, COALESCE(v.version, 'v0.0') as version FROM tenants t LEFT JOIN tenant_schema_version v ON t.id = v.tenant_id WHERE t.is_active = true """) tenants = cur.fetchall() cur.close() conn.close() return tenants def apply_migration(db_name, version): """Apply a single migration to a tenant DB.""" filename = MIGRATIONS[version] filepath = os.path.join(MIGRATIONS_DIR, filename) if not os.path.exists(filepath): print(f" ERROR: Migration file not found: {filepath}") return False with open(filepath) as f: sql = f.read() # Skip migrations marked for manual/non-tenant execution first_line = sql.splitlines()[0].strip() if sql.strip() else "" if first_line.startswith(": SKIP") or first_line.startswith("-- : SKIP"): print(" SKIP (manual/non-tenant migration)") return True conn = get_tenant_conn_by_dbname(db_name) cur = conn.cursor() try: cur.execute(sql) conn.commit() return True except Exception as e: conn.rollback() print(f" ERROR: {e}") return False finally: cur.close() conn.close() def run_migrations(): """Apply pending migrations to all tenants.""" tenants = get_all_tenants() sorted_versions = sorted(MIGRATIONS.keys()) print(f"Found {len(tenants)} active tenants") print(f"Available migrations: {sorted_versions}") for tenant_id, db_name, name, current_version in tenants: print(f"\n[{name}] (db={db_name}, current={current_version})") for version in sorted_versions: if version <= current_version: continue print(f" Applying {version}...", end=" ") if apply_migration(db_name, version): # Update version in master master_conn = get_master_conn() master_cur = master_conn.cursor() master_cur.execute( """ INSERT INTO tenant_schema_version (tenant_id, version) VALUES (%s, %s) ON CONFLICT (tenant_id) DO UPDATE SET version = %s, updated_at = NOW() """, (tenant_id, version, version), ) master_conn.commit() master_cur.close() master_conn.close() print("OK") else: print("FAILED — stopping migrations for this tenant") break print("\nDone.") if __name__ == "__main__": run_migrations()