Módulo 2: Anatomy Of An Iceberg Table

Inspeccionando la tabla de Kiosko en disco

Descripción

Las lecciones 2 a 5 recorrieron la cadena completa con Python: SQL directo sobre el catálogo, json.load() sobre la metadata, table.inspect.manifests()/table.inspect.files() de PyIceberg sobre los manifest y los datos. Esta lección repite el mismo recorrido una vez más, pero esta vez desde la terminal, con ls, file y cat — sin una sola línea de Python. El objetivo es que veas, con tus propios ojos y sin ninguna capa de abstracción de por medio, la diferencia física entre un archivo legible y uno que no lo es, y que confirmes, contando archivos a mano, que nada de lo que el módulo 1 escribió desapareció.

Conexión con el módulo. Esta lección no enseña ningún concepto nuevo — es la misma cadena de las lecciones 2 a 5, verificada con herramientas de sistema operativo genéricas, para que la anatomía de una tabla Iceberg deje de sentirse como "algo que solo PyIceberg puede ver" y se sienta como lo que es: una estructura de archivos real, en un directorio real, que cualquier herramienta de shell puede recorrer.

Una analogía: caminar el pasillo del archivo, en persona

Hasta ahora, cada lección te pidió que le preguntaras al bibliotecario (el catálogo o la API de PyIceberg) por una pieza específica del expediente. Esta lección es distinta: caminas tú mismo el pasillo del archivo, abres el estante con tus propias manos, y miras las carpetas una por una. No necesitas que nadie te entregue nada preparado — simplemente cuentas cuántas carpetas hay, lees las que puedas leer a simple vista, y confirmas que las que no puedes leer, de verdad no se pueden leer sin ayuda especializada.

Ejemplo trabajado: el warehouse completo, desde la terminal

Paso 1 — Lista el árbol completo, con tamaños

ls -la kiosko_warehouse/kiosko/fact_orders/data/
ls -la kiosko_warehouse/kiosko/fact_orders/metadata/

Qué esperar (los nombres con UUID son los de tu propia corrida; la cantidad de archivos y su tipo son deterministas):

kiosko_warehouse/kiosko/fact_orders/data/:
00000-0-<uuid>.parquet   3.2K

kiosko_warehouse/kiosko/fact_orders/metadata/:
00000-<uuid>.metadata.json                     1.1K
00001-<uuid>.metadata.json                     2.1K
<uuid>-m0.avro                                  4.7K
snap-<snapshot_id>-0-<uuid>.avro                1.8K

Cuenta con este listado: un archivo de datos, dos archivos de metadata JSON, un manifest file, un manifest list — cinco archivos en total, para una tabla con un único snapshot. Fíjate en los dos .metadata.json: 00000-... (1.1K, la tabla vacía de la lección 5 del módulo 1) y 00001-... (2.1K, más grande porque ya incluye la sección snapshots[] completa) — ambos siguen existiendo, ninguno fue borrado tras la carga de la lección 6 de ese módulo. Esta es la evidencia más directa y más simple de toda la guía de que "nada se sobrescribe": un ls normal, sin ningún código de Iceberg, ya lo confirma.

Paso 2 — file: identifica el tipo real de cada archivo, sin confiar solo en la extensión

file kiosko_warehouse/kiosko/fact_orders/metadata/*.json \
     kiosko_warehouse/kiosko/fact_orders/metadata/*.avro \
     kiosko_warehouse/kiosko/fact_orders/data/*.parquet

Qué esperar:

kiosko_warehouse/kiosko/fact_orders/metadata/00000-<uuid>.metadata.json: JSON data
kiosko_warehouse/kiosko/fact_orders/metadata/00001-<uuid>.metadata.json: JSON data
kiosko_warehouse/kiosko/fact_orders/metadata/<uuid>-m0.avro: Apache Avro version 1
kiosko_warehouse/kiosko/fact_orders/metadata/snap-<snapshot_id>-0-<uuid>.avro: Apache Avro version 1
kiosko_warehouse/kiosko/fact_orders/data/00000-0-<uuid>.parquet: Apache Parquet

El comando file de Unix no confía en la extensión del nombre del archivo —lee los primeros bytes de cada archivo y reconoce su formato real por su firma interna—. Esta salida confirma, de forma independiente de cualquier convención de nombres, exactamente lo que las lecciones 3, 4 y 5 de este módulo ya demostraron con código: los dos .metadata.json son JSON de verdad, los dos .avro son Avro de verdad, y el .parquet es Parquet de verdad. Ningún archivo miente sobre lo que es.

Paso 3 — Intenta leer el metadata como texto (funciona), y después el manifest (no funciona)

head -c 220 kiosko_warehouse/kiosko/fact_orders/metadata/00001-<tu-uuid>.metadata.json

Qué esperar:

{"location":"file:///.../kiosko_warehouse/kiosko/fact_orders","table-uuid":"<uuid>",...

Legible de inmediato — es exactamente el mismo texto que json.load() parseó sin problema en la lección 3. Ahora, el mismo intento sobre el manifest file:

head -c 220 kiosko_warehouse/kiosko/fact_orders/metadata/<tu-uuid>-m0.avro

Qué esperar (los caracteres ^A, ^N, ^L representan bytes de control no imprimibles — este texto es una transcripción aproximada de lo que verías en tu propia terminal):

Obj^A^N^Lschema<byte-no-imprimible>^G{"type":"struct","fields":[{"id":1,"name":"order_id","type":"string","required":true},{"id":2,"name":"store_id","type":"string","required":true},{"id":3,"name":"product_id","type":"strin

Aquí está la evidencia central de esta lección, en vivo: los primeros bytes (Obj) y un fragmento del encabezado —el esquema Avro embebido, en JSON, que describe la estructura del manifest— sí son legibles, porque Avro guarda su propio esquema como texto al inicio del archivo. Pero apenas termina ese encabezado, el resto del archivo —los datos reales del manifest, comprimidos con el códec deflate— deja de ser texto reconocible. Esto no es un archivo corrupto: es exactamente el comportamiento esperado de un formato binario con compresión, el mismo tipo de formato que ya aceptaste sin cuestionar en un archivo Parquet, ahora visible con tus propios ojos en un manifest Avro.

Diagrama: legible vs. binario, confirmado desde la terminal

flowchart TB
    subgraph LEGIBLE["Legibles con head/cat (texto plano)"]
        A["00000-...metadata.json (1.1K)"]
        B["00001-...metadata.json (2.1K)"]
    end
    subgraph BINARIO["Binarios (Avro/Parquet, necesitan un lector especifico)"]
        C["snap-...avro (1.8K) -- manifest list"]
        D["...-m0.avro (4.7K) -- manifest file"]
        E["00000-0-...parquet (3.2K) -- datos"]
    end
    LEGIBLE -.->|"json.load() (leccion 3)"| OK1["funciona directo"]
    BINARIO -.->|"table.inspect.* / pq.read_table() (lecciones 4-5)"| OK2["funciona con el lector correcto"]
    BINARIO -.->|"cat/head como texto"| FAIL["encabezado legible,\ncuerpo ilegible"]

Profundización: por qué el tamaño de cada archivo cuenta una historia

Vale la pena leer los tamaños del paso 1 con atención, porque no son arbitrarios. 00000-...metadata.json (1.1K) es más chico que 00001-...metadata.json (2.1K) porque el primero describe una tabla vacía —sin ninguna entrada en snapshots[]—, mientras que el segundo ya carga la sección completa del primer snapshot, con su summary de nueve campos. El manifest file (4.7K) es más grande que el manifest list (1.8K) porque el manifest file guarda, por cada archivo de datos que enumera, estadísticas detalladas por columna —los lower_bounds/upper_bounds/null_value_counts que viste como columnas de table.inspect.files() en la lección 5— mientras que el manifest list solo necesita un resumen por manifest file, mucho más compacto. Y el archivo de datos (3.2K) es, de los cinco, el que contiene la mayor cantidad de información real —cuarenta filas completas, siete columnas—, pero gracias a la compresión columnar de Parquet, termina siendo comparable en tamaño al manifest file que solo describe un archivo de datos. Ninguno de estos tamaños es una coincidencia: cada capa de la cadena está diseñada para ser proporcional a la cantidad de información que necesita resumir, no a la cantidad de datos reales que hay debajo.

Errores comunes

Ejecutar estos comandos desde el directorio equivocado, y ver "No such file or directory". Qué pasa: alguien corre ls kiosko_warehouse/... desde un directorio distinto al que usó para el módulo 1, y el comando falla porque esa ruta relativa no existe ahí. Por qué pasa: a diferencia del código Python de este módulo, que usa os.path.abspath() para resolver rutas sin importar el directorio de trabajo (patrón enseñado en la lección 4 del módulo 1), los comandos de shell de esta lección usan rutas relativas literales, que sí dependen de dónde estés parado. Cómo detectarlo: si ls kiosko_warehouse/... falla pero tu script de Python de la lección 2 (que usa la misma ruta, resuelta a absoluta) funciona sin problema, revisa desde qué carpeta estás corriendo el comando de shell. Cómo corregirlo: ubícate, con cd, en el mismo directorio de trabajo donde corriste los scripts del módulo 1 antes de ejecutar los comandos de esta lección — o usa la ruta absoluta completa en cada comando.

Interpretar el encabezado legible de un archivo Avro como evidencia de que "en realidad sí es texto". Qué pasa: alguien, al ver el fragmento de JSON legible al inicio del manifest file en el paso 3, concluye que el archivo completo es, en el fondo, texto plano con algo de ruido. Por qué pasa: ver JSON reconocible genera una falsa sensación de familiaridad. Cómo detectarlo: si intentas parsear el archivo Avro completo con json.load() (como hiciste con el metadata en la lección 3), vas a obtener un error de sintaxis inmediato, apenas termine el encabezado. Cómo corregirlo: el encabezado con el esquema es, a propósito, la única parte de un archivo Avro pensada para ser inspeccionada visualmente por un humano —ayuda a depurar problemas de esquema sin herramientas—; el cuerpo con los datos reales está comprimido y estructurado en binario, y requiere un lector Avro real (como el que table.inspect.manifests() usa internamente) para decodificarse.

Contar archivos con ls y no notar que el conteo de manifest files y manifest lists es distinto al de table.inspect.manifests(). Qué pasa: alguien, al hacer ls sobre la carpeta metadata/, cuenta manualmente los .avro y espera que ese número coincida directamente con table.inspect.manifests().num_rows. Por qué pasa: en el caso de Kiosko con un solo snapshot, ambos números coinciden (hay un manifest list y un manifest file, y table.inspect.manifests() devuelve 1 fila) — así que la confusión pasa desapercibida en este módulo, pero puede llevar a una intuición incorrecta más adelante. Cómo detectarlo: recuerda que ls cuenta archivos en disco, incluidos los manifest lists; table.inspect.manifests() cuenta específicamente manifest files, un subconjunto de lo que ves en ls. Cómo corregirlo: para distinguirlos a simple vista en el listado de ls, usa el patrón de nombre: un archivo que empieza con snap- es un manifest list; un archivo que termina en -m<número>.avro es un manifest file — el ejercicio 2 de esta lección practica exactamente esa distinción.

Ejercicios

Ejercicio 1 — Reproduce el recorrido completo tú mismo, sin Python. En tu propia terminal, con el kiosko_warehouse/ del módulo 1 disponible, corre los tres pasos de esta lección en orden: ls -la, file, y los dos head -c 220. Confirma que cuentas cinco archivos en total, y que el head del .metadata.json es completamente legible mientras que el del .avro se vuelve ilegible después del encabezado.

Ver solución

Si tu tabla tiene el mismo estado que dejó el módulo 1, deberías contar exactamente cinco archivos: un .parquet, dos .metadata.json, un manifest file -m0.avro, y un manifest list snap-*.avro. Si file no está disponible en tu sistema (poco común, pero posible en algunos entornos mínimos), puedes confirmar el mismo resultado revisando los primeros bytes de cada archivo con head -c 10 <archivo> | xxd — los archivos JSON empiezan con {, los Avro empiezan con Obj, y los Parquet empiezan con PAR1.

Ejercicio 2 — Distingue el manifest list del manifest file usando solo el patrón de nombre. Sin ejecutar table.inspect.manifests() de nuevo, mira el ls del paso 1 de esta lección y, usando únicamente el patrón de nombre explicado en el tercer error común, identifica cuál de los dos archivos .avro es el manifest list y cuál es el manifest file.

Ver solución

El archivo que empieza con snap-<snapshot_id>-0-<uuid>.avro es el manifest list —el prefijo snap- seguido del snapshot_id es el patrón que la lección 3 ya identificó dentro del campo manifest-list del archivo de metadata—. El archivo que termina en <uuid>-m0.avro, sin el prefijo snap-, es el manifest file — el sufijo -m0 indica que es el primer (y único, en este caso) manifest file de esa escritura.

Ejercicio 3 — Predicción: ¿qué comando de shell usarías para confirmar, sin Python, que el archivo de datos es Parquet válido y no solo "algo que termina en .parquet"? Sin usar pq.read_table() (eso ya lo hiciste en la lección 5), predice qué comando de esta lección confirma, de forma independiente, que el archivo de datos es Parquet real.

Ver solución

file kiosko_warehouse/kiosko/fact_orders/data/*.parquet — el comando file no confía en la extensión .parquet del nombre; lee la firma binaria real del archivo (los bytes mágicos PAR1 al inicio y al final del archivo, parte de la especificación de Parquet) y confirma independientemente que es "Apache Parquet", sin necesitar ningún lector completo del formato. Es el mismo tipo de verificación —firma binaria, no extensión— que el paso 2 de esta lección ya usó para los archivos .avro y .metadata.json.

Resumen y siguiente paso

En esta lección recorriste el kiosko_warehouse/ completo desde la terminal, sin una sola línea de Python: contaste cinco archivos exactos (un dato, dos metadata, un manifest list, un manifest file), confirmaste con file el tipo real de cada uno, y viste, con tus propios ojos, que un manifest Avro tiene un encabezado legible seguido de un cuerpo binario ilegible sin un lector adecuado — la evidencia física, sin ningún código de Iceberg de por medio, de todo lo que las lecciones 2 a 5 ya demostraron con Python.

Antes de avanzar deberías poder: listar y contar, desde la terminal, todos los archivos de una tabla Iceberg recién cargada; usar file para confirmar el tipo real de un archivo sin confiar en su extensión; y explicar por qué el encabezado de un archivo Avro es parcialmente legible aunque su cuerpo no lo sea.

Recorriste la cadena completa dos veces: una con Python, eslabón por eslabón (lecciones 2 a 5), y otra desde la terminal, de un vistazo (esta lección). La lección 7 vuelve a Python, pero ahora enfocada en un ángulo distinto: no "qué hay en cada archivo", sino "cómo cambia el snapshot con el tiempo" — usando los cuatro métodos de inspección de PyIceberg (table.history(), table.inspect.snapshots(), table.inspect.manifests(), table.inspect.files()) juntos, como el kit de herramientas que vas a usar sin pausa durante el resto de esta guía.

Recursos

  • Comando file de Unix — documentación del manual (man file), la herramienta de identificación de tipo de archivo por firma binaria usada en esta lección. Disponible en cualquier sistema Unix/Linux/macOS. En inglés (páginas de manual del sistema).
  • Apache Avro — especificación del formato de contenedor de archivo (Object Container Files), la sección que explica el encabezado con esquema embebido que esta lección observó parcialmente legible. avro.apache.org/docs/++version++/specification/#object-container-files. En inglés.
  • DISEÑO de esta guía — la instrucción explícita de explorar el warehouse/ en disco como parte verificable del módulo 2. src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.