Elegir espacio

Dos espacios

¿Qué quieres consultar?

Elige el espacio al que quieres entrar.

Biblioteca FastAPIEvoluciona el esquema sin improvisar en producción

Paso 10 · Bloque IV · Unidad 4.4

Evoluciona el esquema sin improvisar en producción

Versiona cambios de schema, revisa autogenerate, preserva datos, hace converger seeds y prepara PostgreSQL con una ejecución coordinada.

1. Predice la transición antes de editar una revisión#

Una migración empieza con cuatro observaciones separadas:

ObjetoPreguntaEvidencia
Modelo actual¿Cómo debería verse el schema destino?Metadata importada por Alembic.
Schema desplegado¿Qué columnas y constraints existen ahora?Inspección del motor, no el archivo Python.
Datos existentes¿Qué significado debe sobrevivir?Filas representativas antes y después.
Revisión actual¿Desde qué nodo parte esta base?alembic current y alembic_version.

Si el modelo cambia name por display_name, el destino no basta. Debes predecir si es un rename, dos campos coexistentes o una eliminación real. La intención determina la transición.

2. Separa el estado deseado de la historia necesaria#

SQLModel.metadata.create_all(engine) recorre metadata y crea tablas ausentes. Es útil para una base desechable o el primer ejercicio, pero no registra desde qué versión llegas ni transforma una tabla existente.

Una revisión Alembic contiene:

  • un identificador revision;
  • uno o varios ancestros en down_revision;
  • operaciones upgrade() hacia delante;
  • un downgrade() solo cuando existe una reversión verdadera.

3. Lee revisión, historia y base como un mismo grafo#

Un entorno mínimo contiene alembic.ini, migrations/env.py, migrations/versions/ y la tabla que Alembic usa para registrar la revisión aplicada.

0001_create_assets  ──→  0002_rename_asset_name  ──→  head
       ↑                           ↑
 schema v1                    schema v2

Los comandos responden preguntas diferentes:

alembic history
alembic current
alembic upgrade head
alembic downgrade -1

history describe los nodos conocidos por el código. current consulta qué nodo registra esa base. upgrade head calcula y ejecuta el camino pendiente. Un exit code cero solo prueba que las operaciones terminaron, no que conservaron significado.

4. Conecta Alembic con toda la metadata#

Autogenerate necesita comparar dos fuentes: el schema inspeccionado mediante la URL configurada y la metadata actual del programa.

from alembic import context
from sqlmodel import SQLModel

from app import models  # registra todas las tablas

target_metadata = SQLModel.metadata

context.configure(
    connection=connection,
    target_metadata=target_metadata,
)

Si falta el import que registra una tabla, su ausencia en metadata puede parecer una intención de borrado. El grafo de imports vuelve a ser parte del comportamiento, igual que en 3.1 y 4.1.

5. Trata autogenerate como un candidato estructural#

alembic revision --autogenerate -m "rename asset name"

Alembic compara diferencias que sabe representar. Puede detectar tablas, columnas, nulabilidad, índices o constraints en determinados casos. No conoce la intención semántica de un cambio.

Para un rename puede proponer algo equivalente a:

def upgrade() -> None:
    op.add_column("assets", sa.Column("display_name", sa.String(), nullable=False))
    op.drop_column("assets", "name")

El schema final coincide con el modelo, pero los valores de name desaparecen. La revisión humana debe sustituir ese par por un rename o una secuencia de expansión, copia y contracción.

Antes de aceptar un candidato, revisa:

  1. cada drop y cada cambio de nulabilidad;
  2. renames posibles y transformaciones de datos;
  3. nombres de constraints e índices;
  4. compatibilidad con el código que convivirá durante la release;
  5. coste o bloqueo probable sobre el volumen real;
  6. prueba desde una copia que represente el schema anterior.

6. Preserva datos con rename o expandir–copiar–contraer#

Cuando el motor y la estrategia permiten un rename directo:

def upgrade() -> None:
    op.alter_column("assets", "name", new_column_name="display_name")


def downgrade() -> None:
    op.alter_column("assets", "display_name", new_column_name="name")

Para un cambio que necesita transformar datos, separa estados:

add nullable → copy/backfill → verify → require NOT NULL → remove legacy field

El orden importa. Añadir status NOT NULL a filas existentes exige decidir primero qué valor corresponde a cada fila. Un default vacío puede hacer que el DDL pase y los datos mientan.

7. Prueba desde el schema anterior con filas#

Una prueba de migración útil no empieza desde una base vacía en head:

command.upgrade(config, "0001")
seed_v1(database_path)
command.upgrade(config, "head")

assert current_revision(database_path) == "0002"
assert read_display_names(database_path) == ["Router", "Laptop"]

La matriz mínima es:

CaminoDemuestraNo demuestra
Base vacía → headLa cadena reconstruye el destino.Que preserve datos previos.
v1 poblada → headSchema y backfill hacia delante.Recuperación operativa completa.
v1 → head → v1Downgrade estructural y datos conservables.Que todo cambio destructivo sea reversible.
SQL offline PostgreSQLQué DDL produciría el dialecto.Locks, tiempos o comportamiento real del servidor.

8. Haz que los datos de referencia converjan#

Un seed idempotente no significa «ignorar todos los conflictos». Debe conocer qué claves posee y a qué estado deben converger.

REFERENCE = {
    "network": "Network infrastructure",
    "mobile": "Mobile device",
}

with engine.begin() as connection:
    for key, label in REFERENCE.items():
        current = connection.execute(
            select(categories).where(categories.c.key == key)
        ).mappings().one_or_none()
        if current is None:
            connection.execute(insert(categories).values(key=key, label=label))
        elif current["label"] != label:
            connection.execute(
                update(categories).where(categories.c.key == key).values(label=label)
            )

La segunda ejecución produce el mismo estado; una fila custom queda fuera de la propiedad del seed y se conserva.

9. Cambia URL, driver y supuestos al preparar PostgreSQL#

SQLite es un archivo local embebido. PostgreSQL es un servidor con conexiones de red, credenciales, concurrencia y un dialecto propio. Cambiar solo la extensión del archivo no es una migración de motor.

La URL de SQLAlchemy hace explícitas dos decisiones:

postgresql+psycopg://user:password@host:5432/database
└ dialect ┘ └ driver ┘

Construye la URL desde settings validados o con sqlalchemy.URL.create() para evitar errores de escaping. El Engine y su pool se crean sin abrir necesariamente una conexión inmediata; el primer uso real de conexión sigue pudiendo fallar por red, credenciales o readiness.

Al cambiar de motor revisa al menos:

  • tipos, defaults y generación de identidad;
  • constraints, nombres e índices realmente migrados;
  • SQL específico de SQLite o PostgreSQL;
  • nulabilidad y comportamiento de valores únicos con null;
  • concurrencia, locks y aislamiento que una prueba SQLite no reprodujo.

10. Asigna un propietario único a la migración#

Una API puede tener varias réplicas. Eso no convierte el startup de cada worker en un coordinador de schema.

release prepared

coordinated step: alembic upgrade head
      ↓ success
start or advance the compatible version

Antes de aplicar declara:

  • revisión origen y destino;
  • actor único que ejecuta;
  • compatibilidad entre código anterior/nuevo y estados intermedios;
  • comprobación posterior;
  • backup, roll-forward o downgrade realmente disponible.

11. Diagnostica por la evidencia que falta#

SíntomaHipótesisPrueba siguiente
Autogenerate propone borrar todas las tablas.Metadata incompleta o URL equivocada.Imprime nombres de metadata y revisa el destino sin mostrar secretos.
Upgrade verde, valores vacíos.Drop/add o default ocultó un rename.Compara filas v1 y head por clave estable.
Seed funciona una vez.Inserción incondicional.Ejecútalo dos veces y revisa claves poseídas.
current no es head.Revisión pendiente o base apuntada incorrecta.Compara current, heads, history y URL efectiva redactada.
SQLite verde, PostgreSQL falla.Supuesto dependiente del dialecto/servidor.Genera SQL del dialecto y después reproduce en PostgreSQL aislado.

12. Detén el alcance donde cambia la capacidad#

Esta unidad posee:

  • historia de revisiones y estado actual;
  • revisión humana de autogenerate;
  • rename, backfill y prueba con datos v1;
  • seed convergente con propiedad explícita;
  • URL/dialecto PostgreSQL y ejecución coordinada conceptual.

Quedan fuera ramas y múltiples heads, administración PostgreSQL, tuning, backup operativo, migraciones largas sin parada, Compose, CI/CD, permisos de release y pruebas de integración con servidor real.

13. Comprueba que puedes preservar significado#

Puedes cerrar 4.4 cuando:

  • explicas por qué create_all() no migra una base existente;
  • relacionas revision, down_revision, history, current y alembic_version;
  • detectas un rename presentado como drop/add;
  • pruebas upgrade desde v1 poblada, no solo desde vacío;
  • justificas qué downgrade es honesto y cuándo necesitas otra recuperación;
  • ejecutas un seed dos veces y preservas filas ajenas;
  • construyes una URL postgresql+psycopg sin versionar ni registrar secretos;
  • declaras quién aplica una revisión una vez por release.

14. Revisa con apoyo y transfiere sin pistas#

La práctica del Bloque IV añade tres intentos:

  1. EX-B4-13: reparar un candidato de rename cuyo schema final oculta pérdida de datos;
  2. EX-B4-14: hacer converger un seed y respetar una fila que no le pertenece;
  3. EX-B4-S02-04: diseñar de forma autónoma rename, status, constraints y backfill de eventos sobre asignaciones v1.

Los starters contienen pruebas visibles, pero no soluciones. Conserva predicción, revisión actual, comandos, DDL, filas antes/después, decisión de recuperación y condición de parada.

15. Fuentes y vigencia#

  • Alembic 1.18.5 · Tutorial

    Entorno, revisiones encadenadas, upgrade, downgrade, current e history.

  • Alembic 1.18.5 · Auto Generating Migrations

    Comparación de metadata y schema, target_metadata, detecciones y límites de autogenerate.

  • SQLAlchemy 2.0.51 · Engine Configuration

    URL dialecto+driver, construcción segura y creación diferida de conexiones.

  • SQLAlchemy 2.0.51 · PostgreSQL

    Dialectos PostgreSQL y uso del driver psycopg.

  • PostgreSQL 18 · Constraints

    Constraints nombradas, checks, uniques y diferencias que el schema versionado debe materializar.