Módulo 2: Anatomy Of An Iceberg Table
Proyecto: la anatomía de la tabla de Kiosko, mapeada
Descripción
Este proyecto cierra el módulo. Recorriste el catálogo (lección 2), el archivo de metadata (lección 3), el manifest list y los manifest files (lección 4), los archivos de datos (lección 5), el mismo recorrido desde la terminal (lección 6), y los cuatro métodos de inspección de PyIceberg juntos (lección 7). Falta un solo paso: un script único que recorre los cinco eslabones de la cadena, de punta a punta, e imprime un mapa completo de la anatomía de kiosko.fact_orders — con assert finales que confirman, de forma automática, que la cadena entera es consistente.
Conexión con el módulo. Este proyecto no introduce ningún concepto nuevo — es la integración final de las siete lecciones anteriores, exactamente como la lección 8 del módulo 1 integró sus propias cinco lecciones centrales. Retoma, de forma literal, la pregunta que abrió este módulo en la lección 1: ¿qué apunta a qué, y por qué nada se sobrescribe nunca? Este proyecto responde esa pregunta con un solo script, corrido de punta a punta, sobre la tabla real de Kiosko.
Una analogía: el mapa completo del expediente, de un solo vistazo
Las lecciones 2 a 7 de este módulo te hicieron caminar el expediente completo, pieza por pieza: el mostrador, la carátula, el índice de pruebas, las carpetas de evidencia, las fotos concretas, y finalmente el sistema de consulta rápida de la corte. Este proyecto es el momento de dibujar, en una sola hoja, el mapa completo de ese recorrido — como el diagrama que un archivista experimentado prepara para un caso complejo, mostrando de un vistazo cómo se conecta cada pieza con la siguiente, sin que nadie tenga que volver a caminar el pasillo entero para entenderlo.
El material: todo lo que este módulo inspeccionó, en un solo lugar
Necesitas, en el mismo directorio de trabajo donde completaste el módulo 1 (con kiosko_catalog.db y kiosko_warehouse/ ya creados):
(directorio del modulo 1, reutilizado sin cambios)
├── kiosko_catalog.db
├── kiosko_warehouse/
│ └── kiosko/fact_orders/...
└── kiosko_table_anatomy_mapped.py (este proyecto)
Este proyecto es de solo lectura — no crea ninguna tabla nueva, no agrega ninguna fila. Si no conservaste el directorio del módulo 1, puedes recrearlo corriendo de nuevo el proyecto de cierre de ese módulo (kiosko_first_iceberg_table.py, lección 8 de M1) en un directorio nuevo antes de continuar.
La solución de referencia, verificada
# kiosko_table_anatomy_mapped.py -- proyecto de cierre del modulo 2
# mapea la cadena completa: catalogo -> metadata -> manifest list -> manifest files -> data files
# sobre kiosko.fact_orders, exactamente como la dejo el modulo 1 (un solo snapshot).
import json
import os
import sqlite3
from urllib.parse import urlparse
from pyiceberg.catalog import load_catalog
def main() -> None:
print("=== Kiosko: anatomia completa de kiosko.fact_orders ===\n")
warehouse_path = os.path.abspath("kiosko_warehouse")
catalog_db_path = os.path.abspath("kiosko_catalog.db")
# --- Eslabon 1: el catalogo -----------------------------------------
conn = sqlite3.connect(catalog_db_path)
cur = conn.cursor()
cur.execute(
"SELECT metadata_location, previous_metadata_location FROM iceberg_tables "
"WHERE table_name = 'fact_orders';"
)
metadata_location, previous_metadata_location = cur.fetchone()
conn.close()
print("Eslabon 1/5 -- catalogo (kiosko_catalog.db)")
print(f" metadata_location: {os.path.basename(urlparse(metadata_location).path)}")
print(f" previous_metadata_location: {os.path.basename(urlparse(previous_metadata_location).path)}")
# --- Eslabon 2: el archivo de metadata --------------------------------
metadata_path = urlparse(metadata_location).path
with open(metadata_path) as f:
metadata = json.load(f)
snap = metadata["snapshots"][0]
print("\nEslabon 2/5 -- archivo de metadata (JSON legible)")
print(f" format-version: {metadata['format-version']}")
print(f" columnas en el esquema: {len(metadata['schemas'][0]['fields'])}")
print(f" cantidad de snapshots: {len(metadata['snapshots'])}")
print(f" snapshot vigente -- operation: {snap['summary']['operation']}, "
f"total-records: {snap['summary']['total-records']}")
# --- Eslabones 3-5: via PyIceberg (manifest list/files, datos) -------
catalog = load_catalog(
"kiosko", type="sql",
uri=f"sqlite:///{catalog_db_path}", warehouse=f"file://{warehouse_path}",
)
table = catalog.load_table("kiosko.fact_orders")
manifests = table.inspect.manifests()
files = table.inspect.files()
history = table.history()
snapshots = table.inspect.snapshots()
print("\nEslabon 3/5 -- manifest list (referenciado desde el snapshot)")
print(f" manifest_list: {os.path.basename(table.current_snapshot().manifest_list)}")
print("\nEslabon 4/5 -- manifest file(s)")
print(f" cantidad de manifest files: {manifests.num_rows}")
for row in manifests.select(["added_data_files_count", "existing_data_files_count"]).to_pylist():
print(f" added_data_files_count={row['added_data_files_count']}, "
f"existing_data_files_count={row['existing_data_files_count']}")
print("\nEslabon 5/5 -- archivo(s) de datos")
print(f" cantidad de archivos de datos: {files.num_rows}")
for row in files.select(["file_format", "record_count"]).to_pylist():
print(f" file_format={row['file_format']}, record_count={row['record_count']}")
print("\n=== Verificacion final: un solo snapshot, cadena consistente ===\n")
print(f"table.history() -- entradas: {len(history)}")
print(f"table.inspect.snapshots() -- filas: {snapshots.num_rows}")
print(f"table.inspect.manifests() -- filas: {manifests.num_rows}")
print(f"table.inspect.files() -- filas: {files.num_rows}")
assert len(history) == 1, f"esperaba 1 entrada de historia, obtuve {len(history)}"
assert snapshots.num_rows == 1, f"esperaba 1 snapshot, obtuve {snapshots.num_rows}"
assert manifests.num_rows == 1, f"esperaba 1 manifest file, obtuve {manifests.num_rows}"
assert files.num_rows == 1, f"esperaba 1 archivo de datos, obtuve {files.num_rows}"
assert table.current_snapshot().snapshot_id == history[-1].snapshot_id
assert previous_metadata_location != metadata_location
total_rows = table.scan().to_arrow().num_rows
assert total_rows == 40, f"esperaba 40 filas, obtuve {total_rows}"
print(f"\nTodas las verificaciones pasaron: cadena consistente, {total_rows} filas, 1 snapshot en todo el modulo.")
if __name__ == "__main__":
main()
Qué esperar (verificado corriendo python3 kiosko_table_anatomy_mapped.py real, sobre el estado exacto que dejó el módulo 1; los nombres de archivo con UUID y el snapshot_id dentro de manifest_list son los de tu propia corrida, distintos cada vez — el resto de la estructura y todos los valores de negocio son deterministas):
=== Kiosko: anatomia completa de kiosko.fact_orders ===
Eslabon 1/5 -- catalogo (kiosko_catalog.db)
metadata_location: 00001-<uuid>.metadata.json
previous_metadata_location: 00000-<uuid>.metadata.json
Eslabon 2/5 -- archivo de metadata (JSON legible)
format-version: 2
columnas en el esquema: 7
cantidad de snapshots: 1
snapshot vigente -- operation: append, total-records: 40
Eslabon 3/5 -- manifest list (referenciado desde el snapshot)
manifest_list: snap-<snapshot-id asignado en tu corrida>-0-<uuid>.avro
Eslabon 4/5 -- manifest file(s)
cantidad de manifest files: 1
added_data_files_count=1, existing_data_files_count=0
Eslabon 5/5 -- archivo(s) de datos
cantidad de archivos de datos: 1
file_format=PARQUET, record_count=40
=== Verificacion final: un solo snapshot, cadena consistente ===
table.history() -- entradas: 1
table.inspect.snapshots() -- filas: 1
table.inspect.manifests() -- filas: 1
table.inspect.files() -- filas: 1
Todas las verificaciones pasaron: cadena consistente, 40 filas, 1 snapshot en todo el modulo.
Fíjate en los cuatro assert intermedios antes del mensaje final: no son decorativos. assert previous_metadata_location != metadata_location confirma, con código —no solo con un ls visual como en la lección 6—, que el catálogo distingue explícitamente "el vigente" de "el anterior", y que ambos son valores distintos, ninguno vacío. Los otros tres assert confirman, con precisión, que los cuatro métodos de la lección 7 —history(), inspect.snapshots(), inspect.manifests(), inspect.files()— coinciden todos en el mismo número: 1. Si cualquiera de los cinco eslabones de la cadena estuviera inconsistente con los demás —por ejemplo, si el catálogo apuntara a un archivo de metadata que dijera "2 snapshots" mientras table.history() solo encontrara 1—, alguno de estos assert fallaría de inmediato.
Diagrama: los cinco eslabones, mapeados en un solo recorrido
flowchart TB
A["Leccion 2:\ncatalogo\n(kiosko_catalog.db)"] --> B["Leccion 3:\narchivo de metadata\n(JSON, 7 columnas, 1 snapshot)"]
B --> C["Leccion 4:\nmanifest list + manifest files\n(Avro, 1 manifest file)"]
C --> D["Leccion 5:\narchivos de datos\n(Parquet, 40 filas)"]
D --> E["Leccion 6:\nmismo recorrido,\ndesde la terminal"]
E --> F["Leccion 7:\nlos 4 metodos de table.inspect\njuntos, consistentes"]
F --> G["Este proyecto:\nun solo script,\nassert automaticos"]
G --> H["Modulo 3:\nsnapshots y time travel\n(un SEGUNDO snapshot, por fin)"]
Cerrando la promesa de la lección 1, punto por punto
| Lo que la lección 1 prometió | Evidencia de que este módulo lo entregó |
|---|---|
| Nombrar los cinco eslabones de la cadena | Lección 1: la analogía del expediente judicial, mapeada eslabón por eslabón |
| Abrir el catálogo, y confirmar que solo guarda una dirección | Lección 2: iceberg_tables con metadata_location/previous_metadata_location, nada más |
| Abrir el archivo de metadata, JSON legible | Lección 3: esquema de 7 columnas, partition-specs vacío, 1 snapshot, todos leídos con json.load() |
| Distinguir manifest list de manifest files, y por qué son Avro | Lección 4: table.inspect.manifests(), 1 manifest file, added_data_files_count=1 |
| Confirmar que los archivos de datos son el mismo Parquet de siempre | Lección 5: pq.read_table() directo, mismo field_id que en el esquema declarado |
| Recorrer la cadena en disco, sin ningún código de Iceberg | Lección 6: ls, file, head — 5 archivos exactos, legibilidad confirmada por tipo |
| Usar la API de inspección de PyIceberg como kit de herramientas | Lección 7: history(), inspect.snapshots(), inspect.manifests(), inspect.files(), consistentes |
| Confirmar que nada se sobrescribió durante todo el módulo | Este proyecto: previous_metadata_location != metadata_location, cadena completa consistente |
Ningún dato de Kiosko cambió durante este módulo — kiosko.fact_orders sigue teniendo exactamente cuarenta filas, el mismo revenue de 106.15 que verificó la lección 7 del módulo 1, y el mismo único snapshot con el que empezó este módulo. Lo que cambió es tu capacidad de responder, con evidencia de primera mano, exactamente qué hay detrás de ese snapshot.
Errores comunes
Correr este proyecto sobre un directorio donde ya avanzaste al módulo 3 (o más adelante). Qué pasa: alguien corre este script después de haber hecho ya una segunda escritura sobre kiosko.fact_orders (adelantándose al módulo 3), y los assert de este proyecto fallan, porque ahora hay más de un snapshot. Por qué pasa: este proyecto asume, a propósito, el estado exacto que deja el módulo 1 — un único snapshot — porque esa es la anatomía más simple posible para aprenderla sin ruido. Cómo detectarlo: si AssertionError: esperaba 1 snapshot, obtuve 2 (o un número mayor) aparece, ya avanzaste el estado de tu tabla más allá de lo que este proyecto espera. Cómo corregirlo: esto no es un error de tu tabla — es exactamente lo esperado si ya hiciste una segunda escritura; corre este proyecto en una copia separada del estado del módulo 1, o simplemente sigue adelante: el módulo 3 va a usar, a propósito, una tabla nueva (kiosko.dim_product) para no interferir con la anatomía de una sola escritura que este proyecto documentó.
Sorprenderse de que el proyecto no crea ninguna tabla ni carga ningún dato nuevo. Qué pasa: alguien, acostumbrado al patrón del proyecto de cierre del módulo 1 —que sí construía una tabla desde cero—, espera que este proyecto haga lo mismo. Por qué pasa: el patrón "proyecto de cierre = construir algo de punta a punta" ya se estableció en el módulo anterior. Cómo detectarlo: si buscas una llamada a catalog.create_table() o table.append() en el script de esta lección y no la encuentras, eso es correcto — no está ahí a propósito. Cómo corregirlo: nada que corregir — este módulo, completo, es de solo lectura sobre lo que el módulo 1 ya construyó; el objetivo es entender la anatomía existente, no crear una anatomía nueva. El módulo 3 retoma la construcción, con una escritura nueva sobre una tabla distinta.
Ejecutar el script sin haber completado el módulo 1 primero. Qué pasa: alguien intenta correr este proyecto en un directorio donde nunca existió kiosko_catalog.db ni kiosko_warehouse/, y el script falla de inmediato al intentar conectarse a una base de datos SQLite vacía o al cargar una tabla que no existe. Por qué pasa: este proyecto, a diferencia del proyecto de cierre del módulo 1, no es autocontenido — depende explícitamente del estado que dejó ese módulo anterior. Cómo detectarlo: si ves un error relacionado con iceberg_tables vacía o NoSuchTableError para kiosko.fact_orders, revisa si el directorio de trabajo tiene el kiosko_catalog.db correcto. Cómo corregirlo: corre primero el proyecto de cierre del módulo 1 (kiosko_first_iceberg_table.py), en el mismo directorio donde vas a correr este script, o copia kiosko_catalog.db y kiosko_warehouse/ desde donde ya lo completaste.
Ejercicios
Ejercicio 1 — Corre el proyecto completo tú mismo, sobre tu propio estado del módulo 1. En el directorio donde completaste el módulo 1, agrega kiosko_table_anatomy_mapped.py y corre python3 kiosko_table_anatomy_mapped.py. Confirma que ves los cinco eslabones y el mensaje final "Todas las verificaciones pasaron".
Ver solución
Si tu tabla kiosko.fact_orders tiene exactamente el estado que dejó el módulo 1 (un append(), cuarenta filas, un snapshot), la salida debería reproducir exactamente la estructura de esta lección: cinco eslabones numerados, seguidos de la verificación final con los cuatro conteos en 1 y el total de 40 filas. Los nombres de archivo con UUID y el snapshot_id dentro del nombre del manifest list van a ser distintos de los mostrados aquí — eso es exactamente lo esperado, no un error.
Ejercicio 2 — Rompe un assert a propósito, y observa el fallo. Cambia temporalmente la línea assert files.num_rows == 1, ... a assert files.num_rows == 99, ..., corre el script de nuevo, y observa qué pasa. Después revierte el cambio.
Ver solución
El script debería fallar con un AssertionError: esperaba 1 archivo de datos, obtuve 1 (el mensaje incluye el valor real encontrado, 1, junto al valor esperado que rompiste a propósito, 99) — la ejecución se detiene justo en ese assert, sin llegar a imprimir el mensaje final de éxito. Este ejercicio confirma, de la misma forma que ya hizo el ejercicio equivalente en el proyecto de cierre del módulo 1, que los assert de este script son verificaciones reales, no decoración — si algún eslabón de la cadena estuviera de verdad inconsistente, el script te lo diría de forma ruidosa e inmediata.
Ejercicio 3 — Explica, en tus propias palabras, por qué este proyecto verifica consistencia entre los cuatro métodos de inspección, y no solo el resultado de cada uno por separado. En 3-4 frases, justifica por qué comparar len(history), snapshots.num_rows, manifests.num_rows y files.num_rows entre sí —todos deben dar 1— es una verificación más fuerte que simplemente confirmar que cada uno, por su cuenta, no lanzó ningún error.
Ver solución
Que un método no lance un error solo confirma que la llamada fue sintácticamente válida y que el archivo correspondiente pudo leerse — no confirma que el contenido de ese archivo sea coherente con el resto de la cadena. Si, por alguna corrupción improbable, el archivo de metadata dijera "hay 2 snapshots" pero el manifest list de uno de ellos hubiera sido borrado por error, table.inspect.snapshots() podría seguir funcionando sin error mientras table.inspect.manifests() fallara o devolviera un número que no cuadra. Comparar los cuatro resultados entre sí —todos deben ser consistentes con la misma cantidad de escrituras reales— es la misma disciplina de "verificar, no confiar" que ya exigió la lección 7 del módulo 1 sobre el revenue de Kiosko, ahora aplicada a la estructura interna de la tabla en vez de a sus valores de negocio.
Resumen y siguiente paso: el cierre de este módulo
Con este proyecto cierras el módulo 2. Recorriste la cadena completa —catálogo → metadata → manifest list → manifest files → archivos de datos— cinco veces, cada vez desde un ángulo distinto: SQL directo, JSON crudo, la API de inspección de PyIceberg, la terminal, y finalmente este script único que junta todo con assert automáticos. Confirmaste, con evidencia de tu propio disco, que kiosko.fact_orders tiene exactamente un snapshot, un manifest file, y un archivo de datos — la anatomía más simple posible, y el punto de partida exacto sobre el que el resto de esta guía construye.
Ninguno de los cuatro problemas nombrados en la lección 2 del módulo 1 (atomicidad, historia, evolución de esquema, partición oculta) está resuelto todavía — eso es exactamente correcto en este punto de la guía. Lo que tienes ahora es algo distinto y necesario: sabes, con precisión de archivo y de campo, dónde vive cada pieza de la verdad sobre una tabla Iceberg, y por qué esa separación en capas es lo que hace posible que el módulo 3 pueda agregar un segundo snapshot sin tocar ni un byte del primero.
Hacia dónde sigues. El módulo 3 —Snapshots y time travel— es donde esta anatomía deja de ser estática por primera vez: vas a crear kiosko.dim_product, sin ninguna columna de historia, hacer un overwrite() con el cambio de P002 (snacks/0.60 → health-snacks/0.68), y usar table.scan(snapshot_id=...) para recuperar el estado anterior — la primera vez en esta guía que vas a ver, con tus propios ojos, dos snapshots coexistiendo en la misma cadena que este módulo acaba de mapear.
Recursos
- PyIceberg — documentación oficial (quickstart) y referencia de API, el flujo completo de inspección que integra este proyecto. py.iceberg.apache.org · py.iceberg.apache.org/api. En inglés.
- Apache Iceberg — documentación oficial, versión de referencia 1.11.0, "Table Spec" completa, la fuente formal de los cinco eslabones que este módulo recorrió. iceberg.apache.org/spec. En inglés.
- DISEÑO de esta guía — el mapa completo de los ocho módulos, incluido el módulo 3 que sigue.
src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.