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:
| Objeto | Pregunta | Evidencia |
|---|---|---|
| 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:
- cada drop y cada cambio de nulabilidad;
- renames posibles y transformaciones de datos;
- nombres de constraints e índices;
- compatibilidad con el código que convivirá durante la release;
- coste o bloqueo probable sobre el volumen real;
- 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:
| Camino | Demuestra | No demuestra |
|---|---|---|
| Base vacía → head | La cadena reconstruye el destino. | Que preserve datos previos. |
| v1 poblada → head | Schema y backfill hacia delante. | Recuperación operativa completa. |
| v1 → head → v1 | Downgrade estructural y datos conservables. | Que todo cambio destructivo sea reversible. |
| SQL offline PostgreSQL | Qué 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íntoma | Hipótesis | Prueba 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,currentyalembic_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+psycopgsin 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:
EX-B4-13: reparar un candidato de rename cuyo schema final oculta pérdida de datos;EX-B4-14: hacer converger un seed y respetar una fila que no le pertenece;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#
Entorno, revisiones encadenadas, upgrade, downgrade, current e history.
Comparación de metadata y schema, target_metadata, detecciones y límites de autogenerate.
URL dialecto+driver, construcción segura y creación diferida de conexiones.
Dialectos PostgreSQL y uso del driver psycopg.
Constraints nombradas, checks, uniques y diferencias que el schema versionado debe materializar.