Módulo 2: Anatomy Of An Iceberg Table

El catálogo: un puntero a la metadata vigente

Descripción

Esta lección abre el primer eslabón de la cadena: el catálogo. Ya lo instalaste en la lección 4 del módulo 1 —un SqlCatalog respaldado por SQLite, kiosko_catalog.db—, pero nunca miraste, literalmente, qué guarda adentro. Esta lección abre esa base de datos con una consulta SQL directa, sin pasar por la API de PyIceberg, y muestra que el catálogo es mucho más simple de lo que su nombre sugiere: no contiene ni una sola fila de datos de Kiosko, ni el esquema de la tabla, ni la lista de snapshots — contiene, únicamente, una fila con un texto: la ruta exacta al archivo de metadata que es, en este momento, "el vigente".

Conexión con el módulo. La lección 1 de este módulo presentó el catálogo como "el mostrador de la corte" en la analogía del expediente judicial — quien te dice a qué carátula ir, sin que tú tengas que buscarla a mano. Esta lección hace esa afirmación literal: vas a leer, con tus propios ojos, la fila exacta de kiosko_catalog.db que cumple ese rol.

Una analogía: el mostrador de la corte no guarda expedientes, guarda direcciones

Sigue con la analogía del módulo. El mostrador de la corte, si le preguntas por el caso "Kiosko contra Nadie" (un nombre inventado, solo para la analogía), no te entrega una copia del expediente completo desde su propio escritorio — te dice: "el expediente vigente está en el archivo, pasillo 3, estante B, carpeta con el sello de hoy". El mostrador guarda una dirección, no el contenido. Si mañana se archiva una versión nueva del expediente, el mostrador actualiza esa dirección —"ahora está en la carpeta con el sello de mañana"—, pero la carpeta de hoy sigue existiendo en el pasillo 3, sin que nadie la haya destruido. Esta lección muestra, en kiosko_catalog.db, exactamente esa misma estructura: una dirección, no un contenido.

Ejemplo trabajado: la fila exacta que registra kiosko.fact_orders

Paso 1 — Consulta kiosko_catalog.db directamente, sin PyIceberg

kiosko_catalog.db es una base de datos SQLite normal — cualquier herramienta que hable SQL puede abrirla, sin necesitar PyIceberg para nada. Esta lección usa el módulo sqlite3 de la librería estándar de Python, precisamente para demostrar que no hace falta ninguna magia especial:

# inspect_catalog_db.py
import os
import sqlite3

catalog_db_path = os.path.abspath("kiosko_catalog.db")

conn = sqlite3.connect(catalog_db_path)
cur = conn.cursor()

cur.execute("SELECT name FROM sqlite_master WHERE type='table';")
print("Tablas internas de kiosko_catalog.db:", [r[0] for r in cur.fetchall()])

cur.execute(
    "SELECT catalog_name, table_namespace, table_name, metadata_location, "
    "previous_metadata_location FROM iceberg_tables;"
)
row = cur.fetchone()
catalog_name, namespace, table_name, metadata_location, previous_metadata_location = row

print("\n=== Fila de kiosko.fact_orders en iceberg_tables ===")
print("catalog_name:", catalog_name)
print("table_namespace:", namespace)
print("table_name:", table_name)
print("metadata_location:")
print(" ", metadata_location)
print("previous_metadata_location:")
print(" ", previous_metadata_location)

conn.close()

Qué esperar (verificado corriendo el script real, sobre el kiosko_catalog.db que dejó el módulo 1; las rutas absolutas van a coincidir con tu propio directorio de trabajo, no con el de este texto):

Tablas internas de kiosko_catalog.db: ['iceberg_tables', 'iceberg_namespace_properties']

=== Fila de kiosko.fact_orders en iceberg_tables ===
catalog_name: kiosko
table_namespace: kiosko
table_name: fact_orders
metadata_location:
  file:///.../kiosko_warehouse/kiosko/fact_orders/metadata/00001-<uuid>.metadata.json
previous_metadata_location:
  file:///.../kiosko_warehouse/kiosko/fact_orders/metadata/00000-<uuid>.metadata.json

Fíjate en las dos columnas que de verdad importan: metadata_location apunta al archivo de metadata con el número más alto00001-..., el que registró el primer snapshot en la lección 6 del módulo 1—; previous_metadata_location apunta al archivo anterior00000-..., el que la lección 5 de ese módulo creó cuando la tabla todavía estaba vacía, con current_snapshot() is None—. Ambos archivos existen todavía en disco, sin que ninguno haya sido borrado — la lección 6 de este módulo lo confirma con un ls real. El catálogo, literalmente, solo lleva la cuenta de "cuál es el vigente" y "cuál era el vigente justo antes" — nada más.

Paso 2 — Confirma lo mismo, ahora con la API de PyIceberg

PyIceberg expone la misma información sin que tengas que escribir SQL a mano:

# via_api.py
import os

from pyiceberg.catalog import load_catalog

warehouse_path = os.path.abspath("kiosko_warehouse")
catalog_db_path = os.path.abspath("kiosko_catalog.db")

catalog = load_catalog(
    "kiosko", type="sql",
    uri=f"sqlite:///{catalog_db_path}", warehouse=f"file://{warehouse_path}",
)
table = catalog.load_table("kiosko.fact_orders")

print("table.metadata_location:")
print(" ", table.metadata_location)

Qué esperar (verificado corriendo el script real):

table.metadata_location:
  file:///.../kiosko_warehouse/kiosko/fact_orders/metadata/00001-<uuid>.metadata.json

El mismo valor exacto que leíste directamente de SQLite en el paso 1 — table.metadata_location no es información distinta, es la misma fila de iceberg_tables, expuesta con una API más cómoda. catalog.load_table("kiosko.fact_orders") hizo, por debajo, exactamente lo que hiciste a mano en el paso 1: consultar iceberg_tables, leer metadata_location, y usar esa ruta para abrir el archivo de metadata —el trabajo de la lección 3—.

Diagrama: qué contiene, y qué NO contiene, el catálogo

flowchart LR
    subgraph DB["kiosko_catalog.db (SQLite)"]
        ROW["fila: kiosko.fact_orders\nmetadata_location -> 00001-...json\nprevious_metadata_location -> 00000-...json"]
    end
    ROW -->|"apunta a"| CURRENT["00001-<uuid>.metadata.json\n(vigente)"]
    ROW -.->|"apuntaba a"| OLD["00000-<uuid>.metadata.json\n(anterior, sigue en disco)"]

No hay ninguna columna en iceberg_tables para schema, snapshots, ni partition-spec — esas tres cosas viven, todas, dentro del archivo JSON al que apunta metadata_location. Esta separación es a propósito: cambiar cuál archivo de metadata es "el vigente" es una operación barata —actualizar un solo texto en una fila de SQLite—, mientras que el archivo de metadata en sí puede crecer con cada snapshot nuevo sin que el catálogo tenga que reescribir nada de su propia estructura.

Profundización: por qué esto es lo que hace posible el control de concurrencia

Vale la pena detenerse en por qué el catálogo guarda solo una ruta, y no el contenido completo de la tabla. Si dos procesos intentan escribir en kiosko.fact_orders al mismo tiempo, cada uno prepara su propio archivo de metadata nuevo sin tocar el de nadie más —eso es barato y no requiere coordinación—; el momento de verdad ocurre cuando cada proceso intenta actualizar la fila metadata_location en iceberg_tables para que apunte a su propio archivo nuevo. Un catálogo respaldado por una base de datos SQL real —como el SqlCatalog de esta guía— puede usar una transacción con una condición ("actualiza metadata_location a mi archivo nuevo, solo si todavía dice lo que yo leí al empezar") para garantizar que, si ambos procesos compiten, exactamente uno gana y el otro recibe un error claro para reintentar. Esta es, con precisión, la garantía que la lección 4 del módulo 1 llamó control de concurrencia optimista — y ahora puedes ver, en la estructura mínima de iceberg_tables, por qué es una operación tan simple de proteger: es un único UPDATE sobre un único texto, no una reescritura de ninguna tabla de datos.

Errores comunes

Buscar el esquema o los datos de la tabla directamente en kiosko_catalog.db. Qué pasa: alguien, después de ver que iceberg_tables existe, espera encontrar ahí columnas con los nombres de order_id, store_id, etc., o incluso filas con datos de Kiosko. Por qué pasa: "catálogo" suena, para quien viene de una base de datos tradicional, como el lugar donde vive todo. Cómo detectarlo: si tu consulta SQL sobre kiosko_catalog.db busca una columna de esquema o de datos y no la encuentra, revisa el ejemplo trabajado de esta lección — iceberg_tables tiene exactamente cinco columnas, ninguna relacionada con el contenido de la tabla. Cómo corregirlo: el esquema vive en el archivo de metadata (lección 3); los datos viven en archivos Parquet (lección 5); el catálogo solo guarda la dirección al primero.

Modificar previous_metadata_location a mano, pensando que "limpia" el catálogo. Qué pasa: alguien, viendo que previous_metadata_location apunta a un archivo de una versión vieja, lo edita o lo borra manualmente en la base de datos SQLite, asumiendo que es basura acumulada. Por qué pasa: el nombre "previous" (anterior) suena a algo que ya no hace falta. Cómo detectarlo: si después de tocar kiosko_catalog.db a mano, catalog.load_table("kiosko.fact_orders") empieza a fallar o a comportarse de forma inesperada, revisa si modificaste esa columna directamente. Cómo corregirlo: nunca edites kiosko_catalog.db a mano fuera de una consulta de solo lectura como la de esta lección — previous_metadata_location es parte de cómo PyIceberg detecta y previene condiciones de carrera; la limpieza real de metadata vieja es una operación explícita (expire_snapshots, módulo 7), no una edición manual de una fila del catálogo.

Asumir que cada tabla del namespace kiosko comparte una sola fila en iceberg_tables. Qué pasa: alguien, después de crear más tablas en módulos posteriores de esta guía (dim_product, dim_store, etc.), espera ver toda la información junta en una sola fila, en vez de una fila por tabla. Por qué pasa: es fácil pensar en "el catálogo de Kiosko" como una unidad singular. Cómo detectarlo: si corres SELECT COUNT(*) FROM iceberg_tables después de crear varias tablas y esperabas ver 1, revisa la clave primaria de la tabla (catalog_name, table_namespace, table_name) en el CREATE TABLE de iceberg_tables. Cómo corregirlo: cada tabla Iceberg —kiosko.fact_orders, y cada tabla que agregues en módulos futuros— tiene su propia fila independiente, con su propio metadata_location; el namespace kiosko es solo un agrupador lógico (lección 5 del módulo 1), no una fusión de sus tablas en el catálogo.

Ejercicios

Ejercicio 1 — Reproduce ambas consultas tú mismo. En tu propia máquina, con kiosko_catalog.db y kiosko_warehouse/ del módulo 1 disponibles, corre el script de sqlite3 y el script de la API de PyIceberg de esta lección. Confirma que metadata_location (de ambos) y table.metadata_location son exactamente el mismo texto.

Ver solución

Si tu tabla kiosko.fact_orders quedó exactamente como la dejó el módulo 1 (un único table.append()), ambos scripts deberían devolver la misma ruta, terminando en 00001-<uuid>.metadata.json. Si tu ruta termina en 00000-..., significa que tu tabla nunca recibió la carga de la lección 6 de M1 — revisa que corriste ese paso antes de empezar este módulo.

Ejercicio 2 — Explica, en tus propias palabras, la diferencia entre metadata_location y previous_metadata_location. Sin mirar la Profundización todavía, escribe 2-3 frases explicando qué guarda cada columna y por qué existen ambas, no solo una.

Ver solución

metadata_location es la dirección al archivo de metadata vigente ahora mismo — el que cualquier lector nuevo debe usar. previous_metadata_location es la dirección al archivo que era vigente justo antes de la última actualización — una sola versión de historia, guardada directamente en el catálogo, útil sobre todo para detectar y depurar condiciones de carrera entre escrituras concurrentes (Profundización de esta lección). La historia completa de todos los snapshots anteriores no vive aquí —serían demasiadas filas para una sola columna—; vive, en cambio, dentro del propio archivo de metadata vigente, en su lista snapshots[] (lección 3).

Ejercicio 3 — Predicción: ¿qué pasaría si borraras kiosko_catalog.db pero dejaras kiosko_warehouse/ intacto? Sin probarlo todavía, predice: si borraras el archivo kiosko_catalog.db (el catálogo), pero conservaras la carpeta kiosko_warehouse/ con todos sus archivos de metadata, manifest y datos, ¿podrías seguir consultando kiosko.fact_orders con catalog.load_table("kiosko.fact_orders")?

Ver solución

No —load_catalog("kiosko", type="sql", uri=f"sqlite:///{catalog_db_path}", ...) recrearía un kiosko_catalog.db completamente nuevo y vacío (SQLite crea el archivo si no existe), sin ninguna fila en iceberg_tables, así que catalog.load_table("kiosko.fact_orders") fallaría con un error de tabla no encontrada, aunque los archivos de datos y de metadata sigan existiendo intactos en kiosko_warehouse/. Esto confirma, con un caso extremo, la Profundización de esta lección: el catálogo no es una copia de la información de la tabla — es el único lugar que sabe dónde encontrarla. Sin él, los datos siguen ahí, pero nadie sabe, sin adivinar, cuál archivo de metadata es el vigente.

Resumen y siguiente paso

En esta lección abriste kiosko_catalog.db directamente con SQL, sin pasar por PyIceberg, y confirmaste que el catálogo guarda exactamente cinco columnas por tabla —ninguna con el esquema o los datos de Kiosko—, con metadata_location como la pieza central: la dirección al archivo de metadata vigente. Confirmaste, con table.metadata_location, que la API de PyIceberg expone la misma información, sin ninguna diferencia.

Antes de avanzar deberías poder: explicar qué contiene y qué NO contiene una fila de iceberg_tables; y explicar por qué separar "la dirección" de "el contenido" hace posible el control de concurrencia.

La lección 3 sigue la flecha: abre, por fin, el archivo al que metadata_location apunta — el archivo de metadata en sí, JSON legible, con el esquema, la partición y la lista de snapshots que este catálogo minimalista nunca guardó directamente.

Recursos

  • PyIceberg — referencia de API, Table.metadata_location y Catalog.load_table(). py.iceberg.apache.org/api. En inglés.
  • Apache Iceberg — documentación oficial, especificación de catálogo (Catalog), la garantía de que una actualización del puntero al metadata es atómica. iceberg.apache.org/spec. En inglés.
  • DISEÑO de esta guía — la elección de SqlCatalog/SQLite y la garantía de control de concurrencia optimista, ya introducida en la lección 4 del módulo 1. src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.