Módulo 7: Messy Domains And Medallion At Depth
Evolución de esquema sin romper gold
Descripción
La lección 5 construyó la alarma: validate_gold_schema() detecta, sin falta, cualquier diferencia entre el esquema real de una tabla gold y el contrato declarado. Pero una alarma que suena cada vez que algo cambia —sin importar si el cambio fue un accidente o una decisión deliberada del negocio— no es suficiente por sí sola. Esta lección resuelve la pregunta que queda pendiente: cuando Kiosko necesita de verdad que fact_orders tenga una columna nueva —por ejemplo, loyalty_points, porque el negocio lanza un programa de fidelidad—, ¿cómo evoluciona el esquema sin que validate_gold_schema() lo trate como una ruptura? La respuesta no es apagar la alarma — es actualizar el esquema y el contrato a la vez, en el mismo cambio.
Conexión con el módulo. Esta lección completa el trabajo de la lección 5 con el otro lado de la moneda: no solo detectar drift accidental, sino distinguirlo de evolución deliberada. Usa las mismas tres discrepancias que la lección 5 ya nombró —columna agregada, columna renombrada, columna con tipo cambiado— pero esta vez muestra, para cada una, el camino seguro y el camino inseguro, lado a lado.
Una analogía: renovar la casa sin perder la escritura
Piensa en la diferencia entre remodelar una casa con permiso de construcción actualizado, y remodelar sin avisarle a nadie. En ambos casos la casa cambia —una pared nueva, una habitación agregada—; la diferencia real está en si el plano oficial de la propiedad —la escritura, los planos registrados— se actualiza al mismo tiempo que la obra, o si la obra avanza sola, dejando el plano desactualizado. Una remodelación con plano actualizado sigue siendo una casa legal, consultable, con historia clara. Una remodelación sin avisar deja, tarde o temprano, una discrepancia que alguien —un inspector, un futuro comprador— va a descubrir de la peor manera posible.
validate_gold_schema() es ese inspector. No le importa si un cambio de esquema es bueno o malo para el negocio —eso lo decide el equipo de datos, no la función—; lo único que verifica es si el "plano" (GOLD_CONTRACTS) coincide con la "obra" (la tabla real). Esta lección enseña a hacer la remodelación con permiso: actualizar la tabla y el contrato en el mismo cambio, nunca uno sin el otro.
Ejemplo trabajado: el mismo cambio, con y sin contrato actualizado
Escenario 1 — Agregar una columna: inseguro vs seguro
Kiosko lanza un programa de fidelidad. fact_orders necesita, de verdad, una columna nueva: loyalty_points. Primero, el camino inseguro —exactamente el que ya viste en la lección 5—:
# unsafe_add_column.py -- continua sobre con y fact_orders reconstruido, con FACT_ORDERS_CONTRACT declarado
con.execute("ALTER TABLE fact_orders ADD COLUMN loyalty_points INTEGER")
print("=== Escenario 1: agregar loyalty_points SIN actualizar el contrato (evolucion insegura) ===")
discrepancies = validate_gold_schema(con, "fact_orders", FACT_ORDERS_CONTRACT)
print(f"Discrepancias: {len(discrepancies)}")
for d in discrepancies:
print(f" - {d}")
Qué esperar.
=== Escenario 1: agregar loyalty_points SIN actualizar el contrato (evolucion insegura) ===
Discrepancias: 1
- fact_orders: columna inesperada 'loyalty_points', no declarada en el contrato
Ahora, el camino seguro: el mismo ALTER TABLE, pero acompañado de la actualización del contrato en el mismo cambio —nunca la tabla sola, nunca el contrato solo—.
# safe_add_column.py -- el mismo ALTER TABLE, con el contrato actualizado a la vez
FACT_ORDERS_CONTRACT_V2 = dict(FACT_ORDERS_CONTRACT)
FACT_ORDERS_CONTRACT_V2["loyalty_points"] = "INTEGER"
print("\n=== Escenario 1b: la misma columna, con el contrato actualizado a la vez (evolucion segura) ===")
discrepancies = validate_gold_schema(con, "fact_orders", FACT_ORDERS_CONTRACT_V2)
print(f"Discrepancias: {len(discrepancies)}")
assert discrepancies == []
print("Verificacion OK: agregar la columna Y actualizar el contrato a la vez mantiene 0 discrepancias")
Qué esperar.
=== Escenario 1b: la misma columna, con el contrato actualizado a la vez (evolucion segura) ===
Discrepancias: 0
Verificacion OK: agregar la columna Y actualizar el contrato a la vez mantiene 0 discrepancias
Ni la tabla ni el contrato cambiaron de forma independiente — cambiaron juntos, en el mismo commit de trabajo (en un repositorio real, en el mismo pull request). FACT_ORDERS_CONTRACT_V2 es, literalmente, FACT_ORDERS_CONTRACT más una línea — la forma más pequeña posible de evolución, y por eso la más segura: agregar una columna nunca rompe ninguna consulta que ya existía, porque nadie que seleccionara columnas explícitas (SELECT order_id, revenue) se ve afectado por una columna nueva que no pidió.
Escenario 2 — Renombrar una columna: por qué siempre rompe algo
Ahora, el caso más peligroso: alguien decide que quantity debería llamarse qty, un nombre más corto.
# unsafe_rename.py -- simulando el renombre en una tabla nueva, con el contrato SIN actualizar
con_renamed = duckdb.connect()
con_renamed.execute("""
CREATE TABLE fact_orders (
order_id VARCHAR, store_id VARCHAR, product_id VARCHAR,
qty INTEGER, unit_price DOUBLE, revenue DOUBLE, order_ts TIMESTAMP, loyalty_points INTEGER
)
""")
print("=== Escenario 2: renombrar quantity a qty (evolucion insegura, ambos lados rotos) ===")
discrepancies = validate_gold_schema(con_renamed, "fact_orders", FACT_ORDERS_CONTRACT_V2)
print(f"Discrepancias: {len(discrepancies)}")
for d in discrepancies:
print(f" - {d}")
Qué esperar.
=== Escenario 2: renombrar quantity a qty (evolucion insegura, ambos lados rotos) ===
Discrepancias: 2
- fact_orders: falta la columna 'quantity' (se esperaba tipo INTEGER)
- fact_orders: columna inesperada 'qty', no declarada en el contrato
Fíjate en que, a diferencia del Escenario 1, aquí no hay una versión "segura" simétrica: actualizar el contrato para que espere qty en vez de quantity haría que validate_gold_schema() vuelva a reportar cero discrepancias, sí —pero cualquier consulta existente en el resto de esta guía, o en cualquier dashboard externo, que escriba SELECT quantity FROM fact_orders se rompería de inmediato, con un error de columna inexistente, sin que validate_gold_schema() tenga ninguna forma de prevenirlo. Un renombre siempre tiene dos víctimas potenciales: el contrato (que este módulo protege) y cualquier consulta externa que use el nombre viejo (que este módulo no puede proteger). Por eso, en un warehouse de producción real, un renombre casi nunca se hace de un solo paso — se hace agregando la columna nueva, migrando a los consumidores durante un período de transición, y solo después eliminando la columna vieja.
Escenario 3 — Cambiar el tipo de una columna: inseguro vs seguro
Por último, un escenario común en producción: quantity empieza a desbordar el rango de INTEGER (Kiosko crece, y alguna orden corporativa pide miles de unidades), así que alguien lo amplía a BIGINT.
# unsafe_type_change.py -- el tipo cambia, el contrato no
con_widened = duckdb.connect()
con_widened.execute("""
CREATE TABLE fact_orders (
order_id VARCHAR, store_id VARCHAR, product_id VARCHAR,
quantity BIGINT, unit_price DOUBLE, revenue DOUBLE, order_ts TIMESTAMP, loyalty_points INTEGER
)
""")
print("\n=== Escenario 3: quantity cambia de INTEGER a BIGINT (evolucion de tipo, insegura sin avisar) ===")
discrepancies = validate_gold_schema(con_widened, "fact_orders", FACT_ORDERS_CONTRACT_V2)
print(f"Discrepancias: {len(discrepancies)}")
for d in discrepancies:
print(f" - {d}")
# El camino seguro: actualizar el contrato en el mismo cambio
FACT_ORDERS_CONTRACT_V3 = dict(FACT_ORDERS_CONTRACT_V2)
FACT_ORDERS_CONTRACT_V3["quantity"] = "BIGINT"
print("\n=== Escenario 3b: mismo cambio de tipo, con el contrato actualizado a BIGINT ===")
discrepancies = validate_gold_schema(con_widened, "fact_orders", FACT_ORDERS_CONTRACT_V3)
print(f"Discrepancias: {len(discrepancies)}")
assert discrepancies == []
print("Verificacion OK")
Qué esperar.
=== Escenario 3: quantity cambia de INTEGER a BIGINT (evolucion de tipo, insegura sin avisar) ===
Discrepancias: 1
- fact_orders: 'quantity' tiene tipo BIGINT, se esperaba INTEGER
=== Escenario 3b: mismo cambio de tipo, con el contrato actualizado a BIGINT ===
Discrepancias: 0
Verificacion OK
A diferencia del renombre, ampliar un tipo (INTEGER a BIGINT, un rango numérico más amplio del mismo tipo de dato) es, casi siempre, seguro para los consumidores existentes — cualquier consulta que ya leía quantity como número entero sigue funcionando igual, porque BIGINT sigue comportándose como un entero, solo que con más rango. Angostar un tipo (por ejemplo, de BIGINT a INTEGER, o de DOUBLE a INTEGER) es la operación peligrosa —puede truncar datos existentes—, y esta guía no la recomienda nunca sin una migración explícita fuera de alcance aquí.
Diagrama: el flujo correcto de una evolución deliberada
flowchart TD
A["El negocio necesita un cambio real\n(ej. loyalty_points)"] --> B{"Que tipo de cambio?"}
B -->|"Agregar columna"| C["ALTER TABLE ADD COLUMN\n+ actualizar GOLD_CONTRACTS\nEN EL MISMO CAMBIO"]
B -->|"Ampliar un tipo"| D["ALTER TABLE ... TYPE\n+ actualizar GOLD_CONTRACTS\nEN EL MISMO CAMBIO"]
B -->|"Renombrar / angostar tipo"| E["Migracion en 2 pasos:\n1. agregar columna nueva,\n2. periodo de transicion,\n3. recien ahi eliminar la vieja"]
C --> F["validate_gold_schema()\n0 discrepancias"]
D --> F
E --> F
Profundización: por qué "aditivo primero" es la regla que sobrevive a la práctica
El patrón que emerge de los tres escenarios de esta lección tiene nombre: evolución aditiva. Agregar una columna nueva nunca rompe nada que ya existía —cualquier consulta que no la pida, simplemente no la ve—. Ampliar un tipo casi nunca rompe nada —el rango crece, no se reduce—. Pero renombrar o eliminar una columna, o angostar un tipo, siempre tiene el potencial de romper algo que ya dependía de la forma anterior, sin que validate_gold_schema() —ni ninguna función de esquema local— pueda prevenirlo del todo, porque el problema no está en la tabla: está en el código de consumo que esta guía no controla.
Esta es exactamente la razón por la que un contrato de esquema local, como el de este módulo, tiene un límite claro: puede confirmar que la forma de una tabla es la esperada, pero no puede rastrear quién más está consultando esa tabla, ni avisarle cuando algo cambia. Un sistema de gobierno de datos real —con catálogo, con linaje, con notificación automática a consumidores— sí puede hacer eso, y es exactamente el territorio de data-reliability-and-governance-guide. Aquí, la disciplina que puedes llevarte es más simple pero igual de valiosa: prefiere siempre agregar sobre renombrar, y ampliar sobre angostar — la regla más barata de aplicar sin ninguna herramienta adicional.
Errores comunes
Actualizar la tabla sin actualizar el contrato, "para después". Qué pasa: alguien agrega una columna real y necesaria a fact_orders, con la intención de actualizar GOLD_CONTRACTS "en un rato", y se le olvida —o lo deja para otro día—. Por qué pasa: el cambio de esquema en sí (el ALTER TABLE) se siente como el trabajo completo, y actualizar un diccionario de Python en otro archivo se siente como un paso administrativo separable. Cómo detectarlo: si corres validate_gold_schema() y ves una discrepancia sobre una columna que tú mismo agregaste a propósito, no encontraste un bug — encontraste tu propio contrato desactualizado. Cómo corregirlo: el ALTER TABLE y la actualización de GOLD_CONTRACTS deben vivir en el mismo cambio, revisados juntos —en un repositorio real, en el mismo commit o pull request—, nunca como dos pasos separados en el tiempo.
Actualizar el contrato sin actualizar la tabla real. Qué pasa: alguien, planeando un cambio futuro, agrega loyalty_points a GOLD_CONTRACTS antes de que la columna exista realmente en fact_orders —"para no olvidarlo después"—. Por qué pasa: parece prudente documentar la intención con anticipación. Cómo detectarlo: si validate_gold_schema() reporta "falta la columna 'loyalty_points'" y tú sabes que esa columna todavía no debería existir, tu contrato está adelantado a la realidad, no la realidad atrasada al contrato. Cómo corregirlo: el contrato describe lo que la tabla es, no lo que planeas que sea — agrégalo al contrato en el mismo momento en que el ALTER TABLE (o el CREATE TABLE) lo hace realidad, nunca antes.
Tratar un renombre como si fuera tan seguro como agregar una columna. Qué pasa: alguien, después de ver que "agregar columna y actualizar el contrato" resuelve el Escenario 1 sin ningún problema, aplica la misma lógica al renombre del Escenario 2 —actualiza GOLD_CONTRACTS para esperar qty en vez de quantity, y da el cambio por completo y seguro—. Por qué pasa: validate_gold_schema() sí vuelve a reportar cero discrepancias después de ese cambio, lo cual se siente como éxito. Cómo detectarlo: si tu "renombre seguro" solo se verificó corriendo validate_gold_schema(), y nunca revisaste si alguna consulta —en esta guía, en un dashboard, en otro script— seguía usando el nombre quantity, tu verificación fue incompleta. Cómo corregirlo: recuerda la distinción exacta de esta lección — un contrato de esquema local puede confirmar que la tabla y el contrato coinciden entre sí, pero no puede confirmar que ningún consumidor externo dependía del nombre anterior. Un renombre necesita, además de actualizar el contrato, un período de transición donde ambos nombres convivan, o una búsqueda manual de quién más consulta esa columna.
Ejercicios
Ejercicio 1 — Simula agregar channel directamente a fact_orders (en vez de usar dim_order_flags), y confirma que el contrato lo detecta como columna inesperada. Usando FACT_ORDERS_CONTRACT (sin loyalty_points), agrega channel como columna VARCHAR a una copia de fact_orders sin actualizar el contrato, y corre la validación.
Ver solución
con_channel = duckdb.connect()
con_channel.execute("""
CREATE TABLE fact_orders (
order_id VARCHAR, store_id VARCHAR, product_id VARCHAR,
quantity INTEGER, unit_price DOUBLE, revenue DOUBLE, order_ts TIMESTAMP, channel VARCHAR
)
""")
discrepancies = validate_gold_schema(con_channel, "fact_orders", FACT_ORDERS_CONTRACT)
print(f"Discrepancias: {len(discrepancies)}")
for d in discrepancies:
print(f" - {d}")
Salida esperada:
Discrepancias: 1
- fact_orders: columna inesperada 'channel', no declarada en el contrato
Esta discrepancia es, en cierto sentido, una segunda alarma útil más allá de la detección de drift accidental: si alguien intentara agregar channel directamente a fact_orders —la alternativa de "columnas sueltas" que la lección 4 ya midió como peor idea que la dimensión junk—, validate_gold_schema() lo señalaría de inmediato como una desviación del contrato declarado, dando al equipo la oportunidad de preguntarse si esa es realmente la forma correcta de agregar el atributo, antes de aceptarlo sin más.
Ejercicio 2 — Diseña la migración de dos pasos para renombrar quantity a qty de forma segura. Sin ejecutar código, describe en 3-4 pasos concretos cómo renombrarías quantity a qty en un warehouse de producción real, sin romper ninguna consulta existente en el proceso.
Ver solución
- Agregar
qtycomo columna nueva (ALTER TABLE fact_orders ADD COLUMN qty INTEGER), poblada con los mismos valores quequantity— ambas columnas conviven, y el contrato se actualiza para esperar ambas. - Anunciar el período de transición a cualquier consumidor conocido (otros scripts, dashboards, la próxima guía que versiona este modelo), con una fecha límite para migrar de
quantityaqty. - Verificar, después del período de transición, que ninguna consulta activa sigue leyendo
quantity—por ejemplo, revisando logs de consultas si el motor los tuviera, o simplemente confirmando manualmente con cada consumidor conocido—. - Recién entonces, eliminar
quantity(ALTER TABLE fact_orders DROP COLUMN quantity) y actualizar el contrato para dejar de esperarla.
Este proceso de varios pasos es, precisamente, lo que un sistema de gobierno de datos formal automatiza —avisos a consumidores, períodos de deprecación, verificación de uso—; aquí se describe manualmente porque está fuera del alcance de esta guía construirlo como sistema.
Ejercicio 3 — Explica, de memoria, por qué "angostar un tipo" es peligroso incluso si validate_gold_schema() no reporta ninguna discrepancia mientras el cambio está en curso. En 2-3 frases, describe qué podría salir mal si quantity pasara de BIGINT a INTEGER en una tabla con datos ya cargados, más allá de lo que un contrato de esquema puede detectar.
Ver solución
Si fact_orders ya tuviera filas con valores de quantity que excedieran el rango de INTEGER (algo posible si la tabla llevaba tiempo usando BIGINT), reducir el tipo de columna podría truncar o rechazar esos valores durante la migración misma —un problema de datos, no de esquema—. validate_gold_schema() solo compara metadatos después de que el cambio ya ocurrió; no tiene forma de simular, de antemano, si los datos existentes caben en el tipo nuevo y más angosto. Por eso angostar un tipo necesita, además de actualizar el contrato, una verificación explícita de que ningún valor actual excede el nuevo rango —una validación de datos, del tipo que validate_orders() de foundations sí hace, no del tipo que este módulo construyó—.
Resumen y siguiente paso
Esta lección completó el argumento del contrato Medallion: agregar una columna nueva y ampliar un tipo son evoluciones seguras cuando se actualizan a la vez que el contrato —nunca la tabla sola, nunca el contrato solo—; renombrar una columna o angostar un tipo son operaciones que ningún contrato de esquema local puede hacer completamente seguras, porque el riesgo real está en consumidores externos que el contrato no puede rastrear. La regla que sobrevive a la práctica, sin necesitar ninguna herramienta adicional: agregar sobre renombrar, ampliar sobre angostar.
Antes de avanzar deberías poder: explicar por qué agregar una columna es la forma más segura de evolución de esquema; describir los pasos de una migración de renombre de dos etapas; y justificar por qué validate_gold_schema(), aunque útil, no puede prevenir todos los riesgos de una evolución de esquema por sí sola.
La lección 8 —el proyecto de cierre de este módulo— integra todo: reconstruye el warehouse completo de Kiosko, construye dim_order_flags desde cero, y corre validate_gold_schema() sobre las cuatro tablas gold de la guía, documentando el resultado en una estructura formal que el módulo 8 —el capstone de toda la guía— va a heredar sin repetir el trabajo.
Recursos
- Databricks — "What is the medallion lakehouse architecture?" — el marco bronze/silver/gold donde esta lección sitúa la evolución de esquema como una disciplina de la capa gold. docs.databricks.com/aws/en/lakehouse/medallion. En inglés.
- DuckDB — documentación oficial del statement
ALTER TABLE(agregar columna, cambiar tipo, renombrar, eliminar) — la referencia completa de las operaciones de esta lección. duckdb.org/docs/current/sql/statements/alter_table. En inglés. - DuckDB — documentación de tipos numéricos (
INTEGER,BIGINT, rangos y conversión implícita) — la base técnica del Escenario 3 de esta lección. duckdb.org/docs/current/sql/data_types/numeric. En inglés. - DuckDB — documentación oficial del cliente Python, la interfaz que ejecuta cada consulta de esta lección. duckdb.org/docs/current/clients/python/overview. En inglés.