# MEJORES PRÁCTICAS PARA MIGRACIONES - xFarma

## 🚨 REGLAS OBLIGATORIAS

### 1. SIEMPRE usar utilidades de seguridad
```python
# ❌ NUNCA hacer esto:
op.add_column('table', sa.Column('col', sa.String(50)))

# ✅ SIEMPRE hacer esto:
from .migration_utils import safe_add_column
safe_add_column('table', sa.Column('col', sa.String(50)))
```

### 2. VALIDAR antes de ejecutar
```python
from .migration_utils import validate_migration_safety

operations = [
    {"type": "add_column", "name": "new_column"},
    {"type": "create_index", "name": "new_index"}
]
warnings = validate_migration_safety("target_table", operations)
if warnings:
    print("⚠️  ADVERTENCIAS:")
    for warning in warnings:
        print(f"   - {warning}")
```

### 3. USAR template como base
```bash
# Copiar template para nueva migración
cp migration_template.py 001_new_feature.py
# Editar y reemplazar placeholders
```

## 🔧 PATRONES SEGUROS

### Agregar Columna
```python
def upgrade():
    # Verificar condiciones antes de agregar
    if not table_exists('target_table'):
        print("❌ Tabla no existe, abortando...")
        return

    new_column = sa.Column('field_name', sa.String(100), nullable=True)
    if safe_add_column('target_table', new_column):
        print("✅ Columna agregada")

        # Crear índice si es necesario
        safe_create_index('ix_target_table_field_name', 'target_table', ['field_name'])

def downgrade():
    # Rollback en orden inverso
    safe_drop_index('ix_target_table_field_name', 'target_table')
    safe_drop_column('target_table', 'field_name')
```

### Crear Tabla
```python
def upgrade():
    if safe_create_table(
        'new_table',
        sa.Column('id', UUID(as_uuid=True), primary_key=True, default=uuid.uuid4),
        sa.Column('name', sa.String(200), nullable=False),
        sa.Column('created_at', sa.DateTime, server_default=sa.func.now()),
        sa.Column('updated_at', sa.DateTime, server_default=sa.func.now(), onupdate=sa.func.now()),
        # CRÍTICO: Usar extend_existing para thread-safety en Render
        {'extend_existing': True}
    ):
        print("✅ Tabla creada")

        # Índices adicionales
        safe_create_index('ix_new_table_name', 'new_table', ['name'])

def downgrade():
    safe_drop_table('new_table')
```

### Migración de Datos Masivos
```python
def migrate_pharmaceutical_data():
    """Migrar datos farmacéuticos en lotes para evitar timeouts"""
    connection = op.get_bind()

    # Contar registros
    total = connection.execute(sa.text("SELECT COUNT(*) FROM sales_data")).scalar()
    batch_size = 1000  # Optimizado para Render

    print(f"📊 Migrando {total} registros farmacéuticos...")

    for offset in range(0, total, batch_size):
        connection.execute(
            sa.text("""
            UPDATE sales_data
            SET national_code = LPAD(national_code, 6, '0')
            WHERE id IN (
                SELECT id FROM sales_data
                ORDER BY id
                LIMIT :batch_size OFFSET :offset
            )
            """),
            {"batch_size": batch_size, "offset": offset}
        )

        progress = min(offset + batch_size, total)
        print(f"📈 {progress}/{total} ({progress/total*100:.1f}%)")
```

## 🏥 PATRONES ESPECÍFICOS FARMACÉUTICOS

### Códigos Nacionales (SIEMPRE string)
```python
# ✅ CORRECTO
national_code = sa.Column(sa.String(20), nullable=True, index=True)

# ❌ INCORRECTO
national_code = sa.Column(sa.Integer, nullable=True)  # NUNCA integer
```

### Campos de Nomenclator/CIMA
```python
# Usar prefijos claros
nomen_nombre = sa.Column(sa.String(500), nullable=True)
nomen_laboratorio = sa.Column(sa.String(200), nullable=True, index=True)
cima_nombre_comercial = sa.Column(sa.String(500), nullable=True)
cima_atc_code = sa.Column(sa.String(20), nullable=True, index=True)
```

### Precios Farmacéuticos
```python
# Precisión decimal apropiada para precios españoles
nomen_pvp = sa.Column(sa.Numeric(10, 4), nullable=True)  # Hasta 999,999.9999€
nomen_precio_referencia = sa.Column(sa.Numeric(10, 4), nullable=True)
```

## 🛡️ SEGURIDAD Y ROLLBACK

### Backup de Datos Críticos
```python
def backup_critical_pharmaceutical_data():
    """Backup antes de modificaciones riesgosas"""
    if not table_exists('backup_product_catalog'):
        op.execute("""
        CREATE TABLE backup_product_catalog AS
        SELECT national_code, nomen_nombre, nomen_laboratorio, created_at
        FROM product_catalog
        WHERE national_code IS NOT NULL
        """)
        print("✅ Backup farmacéutico creado")

def cleanup_backup():
    """Limpiar backup después de verificar integridad"""
    if table_exists('backup_product_catalog'):
        op.execute("DROP TABLE backup_product_catalog")
        print("🧹 Backup eliminado")
```

### Rollback Completo
```python
def downgrade():
    """SIEMPRE implementar rollback completo y probado"""
    print("=== INICIANDO ROLLBACK ===")

    try:
        # Orden inverso: último en agregar, primero en eliminar
        safe_drop_index('ix_newest_index')
        safe_drop_column('table', 'newest_column')
        safe_drop_table('newest_table')

        print("✅ Rollback completado")
    except Exception as e:
        print(f"❌ ERROR en rollback: {e}")
        # Restaurar desde backup si es crítico
        restore_from_backup()
        raise
```

## 🚀 OPTIMIZACIONES RENDER

### Thread-Safety Obligatorio
```python
# SIEMPRE en __table_args__
__table_args__ = (
    Index('ix_custom_index', 'column1', 'column2'),
    # CRÍTICO para multi-worker en Render
    {'extend_existing': True}
)
```

### Lotes Optimizados
```python
# Lotes más pequeños en Render para evitar timeouts
is_render = os.getenv("RENDER") == "true"
batch_size = 300 if is_render else 1000

# Memory cleanup en datasets grandes
import gc
for chunk in chunks:
    process_chunk(chunk)
    gc.collect()  # Obligatorio en Render
    time.sleep(1)  # Evitar sobrecarga
```

## 📝 LOGGING OBLIGATORIO

### Información Detallada
```python
def upgrade():
    print("=== MIGRACIÓN: Agregar campos nomenclator ===")
    print(f"📊 Base de datos: {op.get_bind().engine.url.database}")

    # Información pre-migración
    connection = op.get_bind()
    count = connection.execute(sa.text("SELECT COUNT(*) FROM product_catalog")).scalar()
    print(f"📈 Productos existentes: {count}")

    # Operaciones con logging
    if safe_add_column('product_catalog', new_column):
        print("✅ Campo nomenclator agregado")
    else:
        print("ℹ️  Campo ya existía, omitido")

    print("=== MIGRACIÓN COMPLETADA ===")
```

## ❌ ANTIPATRONES A EVITAR

```python
# ❌ NO verificar existencia
op.add_column('table', sa.Column('col', sa.String(50)))

# ❌ NO manejar errores
try:
    risky_operation()
except:
    pass  # Silenciar errores es PELIGROSO

# ❌ NO implementar rollback
def downgrade():
    pass  # NUNCA dejar rollback vacío

# ❌ Operaciones sin logging
op.create_table('important_table', ...)  # Sin información de progreso

# ❌ Lotes demasiado grandes
for chunk in huge_chunks:  # Puede causar timeout en Render
    process_chunk(chunk)

# ❌ No usar extend_existing
class NewModel(Base):
    __tablename__ = 'new_table'
    # Sin __table_args__ = {'extend_existing': True}
```

## 🔍 VALIDACIÓN Y TESTING

### Pre-Migration Checks
```python
def validate_prerequisites():
    """Validar prerequisitos antes de migrar"""
    if not table_exists('product_catalog'):
        raise ValueError("Tabla product_catalog requerida no existe")

    connection = op.get_bind()

    # Verificar datos críticos
    result = connection.execute(
        sa.text("SELECT COUNT(*) FROM product_catalog WHERE national_code IS NOT NULL")
    )
    if result.scalar() == 0:
        raise ValueError("No hay productos con código nacional")

    print("✅ Prerequisites validados")
```

### Post-Migration Verification
```python
def verify_migration_success():
    """Verificar que la migración fue exitosa"""
    if not column_exists('product_catalog', 'new_field'):
        raise ValueError("Migración falló: columna no existe")

    connection = op.get_bind()

    # Verificar integridad de datos
    result = connection.execute(
        sa.text("SELECT COUNT(*) FROM product_catalog WHERE new_field IS NOT NULL")
    )

    print(f"✅ Migración verificada: {result.scalar()} registros procesados")
```

## 📚 RECURSOS

- **Template**: `migration_template.py`
- **Utilidades**: `migration_utils.py`
- **Modelos**: `backend/app/models/`
- **Esquema**: `docs/DATA_CATALOG.md`

---

> **xFarma Migration Best Practices**
> Última actualización: 2025-09-29
> SEGUIR ESTAS REGLAS EVITA ERRORES DE PRODUCCIÓN
