Módulo 7: Messy Domains And Medallion At Depth
Contratos Medallion entre bronze, silver y gold
Descripción
Foundations ya usó la arquitectura Medallion —bronze, silver, gold— para organizar su pipeline, y esta guía reconstruyó, módulo tras módulo, tablas que viven en esa capa gold: fact_orders, dim_date, fact_sessions, fact_store_activity, y ahora dim_order_flags. Pero ninguna lección, hasta esta, escribió por escrito una regla verificable sobre qué forma debe tener cada tabla gold antes de considerarse publicada. Esta lección resuelve exactamente eso: validate_gold_schema(con, table, expected_columns), una función de Python que compara el esquema real de una tabla contra el esquema esperado —nombres de columna, tipos de dato— y reporta cualquier discrepancia como una lista de texto. Vas a correrla sobre las cuatro tablas gold de esta guía, con cero discrepancias, y vas a usarla para atrapar, de verdad, un cambio de esquema que rompería un reporte silenciosamente si nadie lo verificara.
Conexión con el módulo. Esta lección construye el segundo resultado ejecutable central del módulo: la función que el diseño de esta guía nombra explícitamente, corrida sobre fact_orders, fact_sessions, fact_store_activity y dim_date — las cuatro tablas gold de toda la guía. El "contrato" de esta lección es deliberadamente pequeño: una función local, no un sistema publicado — la frontera exacta que separa esta guía de data-reliability-and-governance-guide.
Una analogía: la lista de empaque, revisada antes de cerrar la maleta
Piensa en cómo alguien prepara una maleta para un viaje importante —una entrevista de trabajo, una boda—: escribe, de antemano, una lista de lo que debe ir adentro —traje, documentos, cargador—, y antes de cerrar la maleta, la compara contra la lista, artículo por artículo. La lista no revisa si el traje está limpio o si el cargador funciona —eso sería una revisión distinta, más profunda—; solo confirma que cada artículo esperado está presente, y que nada inesperado se coló (como un artículo del viaje anterior que nadie sacó). Sin esa lista, alguien podría cerrar la maleta con confianza y descubrir, ya en el aeropuerto, que el cargador se quedó en casa.
validate_gold_schema() es esa lista de empaque, aplicada a una tabla en vez de una maleta. No revisa si los datos dentro de fact_orders son correctos —eso ya lo hicieron validate_orders() en foundations y el assert de grano del módulo 1—; revisa, únicamente, que las columnas esperadas —los "artículos" del esquema— estén todas presentes, con el tipo correcto, y que ninguna columna inesperada se haya colado. Correrla antes de publicar cada tabla gold es, exactamente, revisar la lista antes de cerrar la maleta.
Ejemplo trabajado: validate_gold_schema(), construida y probada
Parte 1 — La función, con DESCRIBE como fuente de verdad
DuckDB expone el esquema real de cualquier tabla con DESCRIBE, un statement que devuelve, por columna, su nombre y su tipo —la misma fuente que ya usaste, sin nombrarla así, cada vez que revisaste una tabla nueva en esta guía—.
# validate_gold_schema.py
import duckdb
def validate_gold_schema(con: duckdb.DuckDBPyConnection, table: str, expected_columns: dict[str, str]) -> list[str]:
"""Compara las columnas reales de `table` (nombre y tipo) contra expected_columns
(dict nombre_columna -> tipo_duckdb). Retorna una lista de discrepancias en texto;
una lista vacia significa que el esquema real coincide exactamente con el esperado.
No valida datos, solo forma -- el contrato de esta guia es local, no un sistema."""
actual_rows = con.sql(f"DESCRIBE {table}").fetchall()
actual_columns = {row[0]: row[1] for row in actual_rows}
discrepancies = []
for column_name, expected_type in expected_columns.items():
if column_name not in actual_columns:
discrepancies.append(f"{table}: falta la columna '{column_name}' (se esperaba tipo {expected_type})")
elif actual_columns[column_name] != expected_type:
discrepancies.append(
f"{table}: '{column_name}' tiene tipo {actual_columns[column_name]}, se esperaba {expected_type}"
)
for column_name in actual_columns:
if column_name not in expected_columns:
discrepancies.append(f"{table}: columna inesperada '{column_name}', no declarada en el contrato")
return discrepancies
Fíjate en la estructura de dos pasadas: la primera recorre expected_columns y busca cada una en actual_columns (detecta columnas faltantes y columnas de tipo incorrecto); la segunda recorre actual_columns y busca cada una en expected_columns (detecta columnas inesperadas, las que nadie declaró pero que igual llegaron a la tabla). Un contrato de esquema que solo hiciera la primera pasada dejaría pasar, sin ninguna alerta, cualquier columna nueva que alguien agregara sin avisar — exactamente el escenario que la lección 7 va a explotar a fondo.
Parte 2 — El contrato declarado, sobre fact_orders
Antes de correr la función sobre las cuatro tablas gold, pruébala sobre una sola —fact_orders, reconstruida exactamente como quedó desde el módulo 1—, con su contrato de esquema declarado explícitamente:
# fact_orders_contract.py -- continua sobre con, con fact_orders ya reconstruido (modulos 1-4)
FACT_ORDERS_CONTRACT = {
"order_id": "VARCHAR", "store_id": "VARCHAR", "product_id": "VARCHAR",
"quantity": "INTEGER", "unit_price": "DOUBLE", "revenue": "DOUBLE", "order_ts": "TIMESTAMP",
}
discrepancies = validate_gold_schema(con, "fact_orders", FACT_ORDERS_CONTRACT)
print(f"fact_orders: {len(discrepancies)} discrepancias")
for d in discrepancies:
print(f" - {d}")
assert discrepancies == [], "el contrato de fact_orders se rompio"
print("Verificacion OK: fact_orders cumple su contrato de esquema, sin ninguna discrepancia")
Qué esperar.
fact_orders: 0 discrepancias
Verificacion OK: fact_orders cumple su contrato de esquema, sin ninguna discrepancia
Cero discrepancias — la lista de empaque completa, sin faltantes y sin sorpresas. Ahora, la prueba de que la función realmente detecta algo cuando el esquema sí cambia: simula, a propósito, que alguien agregó una columna a fact_orders sin actualizar el contrato.
# schema_drift_demo.py -- simula un cambio de esquema no anunciado
con.execute("ALTER TABLE fact_orders ADD COLUMN loyalty_points INTEGER")
discrepancies_after_drift = validate_gold_schema(con, "fact_orders", FACT_ORDERS_CONTRACT)
print(f"fact_orders (despues del ALTER TABLE): {len(discrepancies_after_drift)} discrepancias")
for d in discrepancies_after_drift:
print(f" - {d}")
Qué esperar.
fact_orders (despues del ALTER TABLE): 1 discrepancias
- fact_orders: columna inesperada 'loyalty_points', no declarada en el contrato
validate_gold_schema() detectó, de inmediato, la columna nueva que nadie declaró — exactamente el tipo de cambio silencioso que, sin esta verificación, se propagaría a cualquier reporte que consultara fact_orders con un SELECT *, sin que nadie se enterara hasta que algo se rompiera río abajo.
El contrato completo: las cuatro tablas gold de la guía
Con la función ya probada sobre fact_orders, declara el contrato completo de las cuatro tablas gold de esta guía —reconstruidas sin el ALTER TABLE de la demostración anterior— y corre la validación sobre las cuatro a la vez.
# gold_contracts.py -- reconstruye las 4 tablas gold sin el ALTER TABLE de la demostracion, y valida
GOLD_CONTRACTS = {
"fact_orders": {
"order_id": "VARCHAR", "store_id": "VARCHAR", "product_id": "VARCHAR",
"quantity": "INTEGER", "unit_price": "DOUBLE", "revenue": "DOUBLE", "order_ts": "TIMESTAMP",
},
"fact_sessions": {
"session_id": "VARCHAR", "store_id": "VARCHAR", "session_date": "DATE",
"view_ts": "TIMESTAMP", "add_to_cart_ts": "TIMESTAMP", "purchase_ts": "TIMESTAMP",
"is_converted": "BOOLEAN",
},
"fact_store_activity": {
"store_id": "VARCHAR", "activity_date": "DATE", "daily_revenue": "DOUBLE",
"revenue_array_7d": "DOUBLE[]", "active_days_7d": "INTEGER",
"revenue_array_30d": "DOUBLE[]", "active_days_30d": "INTEGER",
},
"dim_date": {
"date_key": "INTEGER", "calendar_date": "DATE", "day_of_week": "VARCHAR",
"month": "INTEGER", "quarter": "INTEGER", "year": "INTEGER", "is_weekend": "BOOLEAN",
},
}
print("=== validate_gold_schema(): contrato corrido sobre las 4 tablas gold de la guia ===")
total_discrepancies = 0
for table, expected_columns in GOLD_CONTRACTS.items():
discrepancies = validate_gold_schema(con, table, expected_columns)
total_discrepancies += len(discrepancies)
status = "OK, 0 discrepancias" if not discrepancies else f"{len(discrepancies)} discrepancias"
print(f" {table:20} {status}")
for d in discrepancies:
print(f" - {d}")
assert total_discrepancies == 0, "el contrato Medallion se rompio: hay discrepancias de esquema"
print(f"\nVerificacion: {len(GOLD_CONTRACTS)} tablas gold, {total_discrepancies} discrepancias en total -- OK")
Qué esperar.
=== validate_gold_schema(): contrato corrido sobre las 4 tablas gold de la guia ===
fact_orders OK, 0 discrepancias
fact_sessions OK, 0 discrepancias
fact_store_activity OK, 0 discrepancias
dim_date OK, 0 discrepancias
Verificacion: 4 tablas gold, 0 discrepancias en total -- OK
Cuatro tablas gold, veintiocho columnas en total entre las cuatro, cero discrepancias — el contrato completo de la capa gold de Kiosko, verificado con una sola función, sin revisión manual. Fíjate en algo importante: dim_order_flags, la dimensión construida en la lección anterior, no aparece en este contrato — no porque no importe, sino porque el diseño de esta guía define las cuatro tablas gold verificadas aquí como fact_orders, fact_sessions, fact_store_activity y dim_date, con dim_order_flags como una dimensión más pequeña y de apoyo. Nada te impide extender GOLD_CONTRACTS con su propio esquema —el ejercicio 2 de esta lección lo hace—.
Diagrama: dónde vive el contrato, entre qué capas
flowchart LR
B["BRONZE\nCSV crudo, particionado\npor fecha (foundations)"] --> S["SILVER\nvalidate_orders() +\ntransform_fact_orders()\n(foundations)"]
S --> G["GOLD\nfact_orders, fact_sessions,\nfact_store_activity, dim_date\n(esta guia)"]
G -.->|"validate_gold_schema()\nANTES de publicar"| G
G --> BI["Consumidores de BI\n(dashboards, reportes)"]
Profundización: por qué el contrato vive en gold, no en bronze ni en silver
Vale la pena ser explícitos sobre dónde este módulo coloca la verificación, porque no es arbitrario. Bronze es, por diseño —tal como lo construyó foundations—, tolerante a cualquier forma: recibe el dato crudo tal como llega, sin ninguna garantía de esquema, precisamente porque su trabajo es preservar lo que llegó, no juzgarlo. Silver ya aplica una primera compuerta de calidad —validate_orders()— pero sobre datos, no sobre esquema: filas con campos vacíos, tipos inválidos, duplicados — nunca column faltante o de tipo incorrecto, porque silver en foundations siempre escribe con un esquema fijo que su propio código controla.
Gold es distinto: es la capa que otros equipos consumen directamente —dashboards, reportes, la próxima guía de esta serie (dbt-analytics-engineering-guide, que versiona este mismo modelo como código)—, y es exactamente ahí donde un cambio de esquema no detectado hace más daño, porque se propaga a consumidores que ni siquiera saben que el warehouse cambió. Por eso validate_gold_schema() se corre, precisamente, sobre la capa gold: es el último punto de control antes de que el dato salga del control directo del equipo que lo modela, hacia gente que confía en que la forma no cambió sin avisar.
Errores comunes
Pensar que validate_gold_schema() reemplaza a validate_orders() de foundations. Qué pasa: alguien, al ver dos funciones con nombres parecidos ("validate"), asume que una reemplaza a la otra, o que ya no hace falta correr validate_orders() si validate_gold_schema() existe. Por qué pasa: ambas empiezan con "validate", y ambas aparecen en el mismo pipeline conceptual bronze→silver→gold. Cómo detectarlo: si dejas de correr validate_orders() sobre bronze pensando que validate_gold_schema() ya cubre esa validación, vas a dejar pasar filas con quantity negativo o campos vacíos directamente hasta silver, sin ninguna compuerta. Cómo corregirlo: recuerda la distinción exacta de esta lección — validate_orders() valida datos (¿esta fila es correcta?), corre entre bronze y silver; validate_gold_schema() valida esquema (¿esta tabla tiene la forma correcta?), corre sobre gold, antes de publicar. Son complementarias, no sustitutas.
Declarar expected_columns copiando el esquema real de la tabla, en vez de declararlo de forma independiente. Qué pasa: alguien, para "ahorrar tiempo", genera expected_columns corriendo DESCRIBE sobre la tabla ya construida, en vez de escribir el contrato a mano, de forma independiente, antes de mirar la tabla. Por qué pasa: copiar el esquema actual garantiza que la primera corrida de validate_gold_schema() va a dar cero discrepancias, lo cual se siente como éxito inmediato. Cómo detectarlo: si tu expected_columns siempre coincide exactamente con la tabla real, sin ninguna excepción, es una señal de que nunca vas a detectar nada — el contrato se volvió un espejo de la realidad, no una expectativa independiente que la realidad debe cumplir. Cómo corregirlo: declara expected_columns antes de construir la tabla, o al menos de forma independiente a su esquema actual —como hizo esta lección, escribiendo FACT_ORDERS_CONTRACT a partir de la definición que el módulo 1 ya estableció, no leyéndolo de la tabla en el momento—. Solo así el contrato puede fallar de verdad cuando algo cambia.
Ejecutar validate_gold_schema() una sola vez, al final del proyecto, en vez de antes de cada publicación. Qué pasa: alguien corre la validación una única vez, al terminar de construir el warehouse completo, y asume que con eso el contrato queda "cumplido para siempre". Por qué pasa: correr algo una vez y ver "0 discrepancias" se siente como un problema resuelto de forma permanente. Cómo detectarlo: si alguien modifica fact_orders —agrega una columna, cambia un tipo— seis meses después, y nadie vuelve a correr validate_gold_schema(), la discrepancia nunca se detecta, exactamente como demostró el ALTER TABLE de esta lección. Cómo corregirlo: el valor real de un contrato de esquema no está en correrlo una vez —está en correrlo cada vez que la capa gold se reconstruye o se publica, como parte del pipeline mismo, no como un chequeo manual ocasional. Esta guía no llega a automatizar ese "cada vez" —eso es orquestación real, terreno de airflow-and-declarative-orchestration-guide—, pero la función en sí está diseñada para correr en cada publicación, no una sola vez.
Ejercicios
Ejercicio 1 — Extiende el contrato con dim_order_flags y valídalo. Declara DIM_ORDER_FLAGS_CONTRACT con sus tres columnas (flag_key: INTEGER, payment_method: VARCHAR, channel: VARCHAR), agrégalo a GOLD_CONTRACTS, y confirma que también pasa sin discrepancias.
Ver solución
GOLD_CONTRACTS["dim_order_flags"] = {
"flag_key": "INTEGER", "payment_method": "VARCHAR", "channel": "VARCHAR",
}
discrepancies = validate_gold_schema(con, "dim_order_flags", GOLD_CONTRACTS["dim_order_flags"])
print(f"dim_order_flags: {len(discrepancies)} discrepancias")
assert discrepancies == []
Salida esperada:
dim_order_flags: 0 discrepancias
dim_order_flags también cumple su contrato — nada en el diseño de validate_gold_schema() la limita a las cuatro tablas que esta lección validó explícitamente; cualquier tabla de Kiosko puede tener su propio contrato declarado, se llame o no una de "las cuatro tablas gold de la guía".
Ejercicio 2 — Simula que alguien renombra quantity a qty en fact_orders, y observa las dos discrepancias que produce. Sin usar ALTER TABLE ... ADD COLUMN como en el ejemplo trabajado, usa ALTER TABLE fact_orders RENAME COLUMN quantity TO qty y corre validate_gold_schema() de nuevo.
Ver solución
con.execute("ALTER TABLE fact_orders RENAME COLUMN quantity TO qty")
discrepancies = validate_gold_schema(con, "fact_orders", FACT_ORDERS_CONTRACT)
print(f"fact_orders (despues del RENAME): {len(discrepancies)} discrepancias")
for d in discrepancies:
print(f" - {d}")
Salida esperada:
fact_orders (despues del RENAME): 2 discrepancias
- fact_orders: falta la columna 'quantity' (se esperaba tipo INTEGER)
- fact_orders: columna inesperada 'qty', no declarada en el contrato
Un RENAME produce dos discrepancias, no una: la función no tiene forma de saber que qty "es" quantity con otro nombre —desde su perspectiva, quantity simplemente desapareció (primera discrepancia) y una columna nueva e inesperada, qty, apareció en su lugar (segunda discrepancia)—. Esto es exactamente lo esperado de una validación de esquema por nombre: no infiere intención, solo compara forma contra forma.
Ejercicio 3 — Explica, de memoria, por qué validate_gold_schema() no recibe ningún parámetro de fecha o de rango de filas. En 2-3 frases, explica por qué esta función, a diferencia de casi todo el código ejecutable del resto de la guía, no necesita ningún dato de negocio como entrada.
Ver solución
validate_gold_schema() opera exclusivamente sobre metadatos —el resultado de DESCRIBE—, nunca sobre las filas de la tabla, así que no le importa cuántas filas tiene fact_orders hoy, ni qué fechas cubre, ni si el revenue es 106.15 o cualquier otro número. Su pregunta es enteramente estructural: ¿existen las columnas esperadas, con los tipos esperados, y no existe ninguna columna de más? Esa pregunta tiene la misma respuesta sin importar si la tabla tiene cuarenta filas o cuarenta millones, lo cual es, precisamente, la razón por la que esta función escala sin cambios a cualquier volumen de datos —a diferencia de, por ejemplo, validate_orders(), que sí procesa fila por fila.
Resumen y siguiente paso
Esta lección construyó validate_gold_schema(): una función que compara el esquema real de una tabla —vía DESCRIBE— contra un contrato declarado explícitamente, y reporta cualquier columna faltante, de tipo incorrecto o inesperada como una lista de discrepancias en texto. Corrida sobre las cuatro tablas gold de esta guía —fact_orders, fact_sessions, fact_store_activity, dim_date—, confirmó cero discrepancias; simulada contra un ALTER TABLE no anunciado, detectó de inmediato la columna nueva. Esto es, con precisión, el "contrato" del que habla el título de este módulo: una función local, corrida sobre metadatos, no un sistema publicado de gobierno de datos.
Antes de avanzar deberías poder: escribir de memoria la estructura de dos pasadas de validate_gold_schema() (columnas faltantes/incorrectas, columnas inesperadas); explicar por qué el contrato vive en gold y no en bronze ni silver; y describir la diferencia entre validar datos (validate_orders()) y validar esquema (validate_gold_schema()).
La lección 6 usa dim_date —una de las cuatro tablas de este contrato— para su propósito conformado completo: unir los tres hechos de Kiosko contra el mismo calendario, algo que ningún módulo anterior hizo con las tres tablas a la vez.
Recursos
- Databricks — "What is the medallion lakehouse architecture?" — la definición oficial de bronze/silver/gold que esta lección profundiza con un contrato verificable entre silver y gold. docs.databricks.com/aws/en/lakehouse/medallion. En inglés.
- DuckDB — documentación oficial del statement
DESCRIBE(metadatos de esquema: nombre de columna, tipo), la fuente de verdad devalidate_gold_schema(). duckdb.org/docs/current/guides/meta/describe. En inglés. - DuckDB — documentación oficial del statement
ALTER TABLE(agregar, renombrar, cambiar tipo de columna), usada en esta lección para simular cambios de esquema no anunciados. duckdb.org/docs/current/sql/statements/alter_table. 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.