Published

El tercer pilar · La relación aplicación ↔ Warehouse — estado del arte del layer SQL

Connect any source, model it as an ontology, transform it, and operationalize it, analytics, automation and machine learning, under one governed, self-hostable roof. --- Most teams stitch the...

El tercer pilar · La relación aplicación ↔ Warehouse — estado del arte del layer SQL

Investigación (2026-07-31). Punto de partida: el SQL Editor no ve el warehouse de
producción. La causa inmediata está medida y cerrada (§1). Lo que sigue es el estado del
arte del layer que aún no tenemos: cómo Databricks, Snowflake, Trino, Spark y el mundo
DataFusion resuelven la relación entre una aplicación y un catálogo Iceberg.
Cruzar con: warehouse-index.md · junction.md ·
duckdbengine.md · warehouse-as-narrow-waist.md.
Memoria: warehouse-index-pillar, warehouse-narrow-waist, router-junction.


1 · El hallazgo que desbloquea hoy — un flag de ATTACH

El SQL Editor veía 23 tablas (datasets 12 · bench 11) mientras el warehouse vivo tiene 78 en main.default. La hipótesis natural —«DuckDB mapea namespace→schema y un schema no puede llevar un punto, así que los anidados son irrepresentables»— es falsa. DuckDB sí los representa, como schema citado; lo que pasa es que no los pide salvo que se lo indiques.

duckdb-iceberg expone una opción de ATTACH, y su default es no:

// src/include/iceberg_attach.hpp:31
bool support_nested_namespaces = false;

Su propio test de regresión fija la sintaxis resultante:

ATTACH '' AS my_datalake (TYPE ICEBERG,, SUPPORT_NESTED_NAMESPACES true);
SELECT * FROM my_datalake."level1.level2.level3".nested_namespaces;

Medido contra NUESTRO Lakekeeper, con la misma versión que corre en el servicio (DuckDB 1.5.4), listando sólo catálogo (sin tocar R2):

SUPPORT_NESTED_NAMESPACESSchemas visiblesmain.defaultmain.test
falselo que corre hoybench, datasets, main, testno existeno existe
true+ main.default, main.test, test.w_7b500f4d…78 tablas42 tablas

Nuestro ATTACH (services/duck-server/app/duck.py:536) no lo pasa. De ahí todo lo demás: main aparece como schema vacío (es un namespace padre sin tablas propias), y cualquier referencia a lake."main.test".ds_… no resuelve — DuckDB acaba cayendo al endpoint S3 por defecto (s3.us-west-2.amazonaws.com, error observado InvalidAccessKeyId) en vez de fallar limpio. El 403 a AWS y los cuelgues eran síntomas aguas abajo, no la enfermedad.

Fix: una línea en el ATTACH. No es arquitectura — es config. Pero deja al descubierto dos cosas que sí lo son, y son el resto de este documento.

⚠️ Y un aviso que ya estaba escrito en INFRA.md: ni DuckDB ni su extensión están
pineados. Un default de un flag como éste puede cambiar entre despliegues sin que nadie
toque el repo. El pineado deja de ser higiene y pasa a ser requisito.


2 · El choque estructural: profundidad de namespace

Es el problema de interoperabilidad del ecosistema Iceberg, y todo el mundo lo ha sufrido.

El spec permite profundidad arbitraria. Un namespace Iceberg es una lista de niveles, y en la REST API se codifican en la URL separados por el carácter unidad 0x1F (%1F) — justamente para que el punto quede libre como carácter de nombre. Lo verificamos: nuestro Lakekeeper responde a /namespaces/main%1Ftest/tables sin despeinarse.

Los motores SQL, en cambio, tienen identificadores de tres partes. catalog.schema.table. Ahí es donde se rompe, y cada uno cedió por un sitio distinto:

Motor / plataformaCómo lo resolvió
TrinoFallaba: SHOW SCHEMAS sólo devolvía el nivel alto y USE rechazaba la notación con punto. Causa raíz: mandaba namespaces/a.b en vez de namespaces/a%1Fb. Corregido (#22916 → PR #23453)
SparkSHOW SCHEMAS hace una petición de listado y devuelve sólo el nivel alto; los hijos hay que pedirlos explícitamente al padre. El spark_catalog de sesión exige namespace de una sola parte (requires a single-part namespace, but got [hive, test]) — se esquiva usando un SparkCatalog propio
Snowflake Open Catalog (Polaris)Soporta consultar tablas bajo namespaces anidados y crearlos
StarRocksTuvo que añadir soporte multi-nivel para poder leer las tablas de Snowflake (#52451)
DuckDBLo soporta opt-in (SUPPORT_NESTED_NAMESPACES, default false); el schema anidado se direcciona citado: cat."a.b".tbl
Apache Iceberg 1.7.0El soporte de anidados llegó roto (#11539)

La lectura para nosotros. Nadie ha «resuelto» esto en el motor: todos convergen en que la superficie SQL es de tres niveles y la profundidad extra es un detalle de codificación que alguien tiene que traducir. Databricks (catalog.schema.table), Snowflake (db.schema.table), Trino (catalog.schema.table) y Spice AI (Catalog → Schema → Table) exponen exactamente tres. Nuestro norte Warehouse › Catalog › Schema › Table ya está alineado con el estado del arte — no es una excentricidad que haya que defender, es la convención.

Lo que el estado del arte dice es quién hace la traducción: no el motor. Ver §3.


3 · Quién contesta las preguntas de catálogo — la decisión de fondo

Aquí está el precedente más valioso, y confirma la intuición de no depender del information_schema de DuckDB.

Unity Catalog sirve el INFORMATION_SCHEMA él mismo. Cada catálogo de UC incluye automáticamente un INFORMATION_SCHEMA —vistas de metadata de sólo lectura, nombre reservado— que describe los objetos de ese catálogo. Y dos propiedades que son el verdadero motivo de que lo sirva el catálogo y no el motor:

  • Es consciente de privilegios: filtra automáticamente y sólo muestra los objetos que el principal tiene derecho a ver.
  • system.information_schema agrega el metastore entero, con el mismo filtrado.

Un information_schema servido por el motor no puede hacer ninguna de las dos: el motor no sabe quién pregunta ni qué le está permitido. Es el mismo argumento que ya nos llevó a poner la gobernanza en la puerta y dejar al motor ciego — sólo que aplicado a la metadata en vez de a los datos.

Consecuencia de diseño. Las preguntas de catálogo (SHOW SCHEMAS/TABLES, DESCRIBE, information_schema) las contesta Index, no el motor. El motor sólo debe recibir identificadores físicos ya resueltos. Eso es exactamente lo que la route del SQL Editor ya hace hoy con sus ramas de catálogo (RENAME / CREATE-DROP VIEW / DESCRIBE / SHOW / information_schema), y la regla de frontera de junction.md §0 ya lo dice: el catálogo no es la puerta. Lo que falta no es cambiar ese diseño: es completarlo — hoy buildInfoSchemaRows enriquece longitudes/precisiones llamando a duckColumns, es decir, aún le pregunta al motor por metadata que el catálogo conoce.

Y hay un corolario que resuelve el choque de §2 sin pelearse con nadie: si Index posee el mapa coordenada lógica → identidad física, la profundidad del namespace Iceberg deja de ser un problema de superficie. Ya tenemos esa fórmula determinista ({catalog.slug}.{schema.slug}.ds_<uuid>) y ya la comparten writer.py y fqn.ts bit a bit. El namespace anidado es una consecuencia de haberla adoptado, no un accidente.


4 · Los motores: qué hay maduro en 2026

MotorLectura IcebergEscrituraNota
DuckDB + icebergwrite 1.4.0 · UPDATE/DELETE 1.4.2 · MERGE INTO, ALTER TABLE, partition transforms, V3 en 1.5.3Ciclo de vida completo desde un proceso embebido. Sigue etiquetado experimental; ni él ni la extensión están pineados en nuestro despliegue
DataFusion + iceberg-rustsí, con pushdown de predicado y de LIMIT0.9.0: CREATE TABLE, DROP TABLE, INSERT INTO con clustering por orden en particionadasSin mutación de fila (UPDATE/DELETE) todavía. Databricks y el propio proyecto Iceberg han convergido en DataFusion como backend de su tooling en Rust
Spark + Iceberg runtimereferenciacompletaEl estándar de facto; el coste es operativo (cluster), no de capacidad
TrinoDistribuido, orientado a analítica interactiva; anidados ya corregidos

Substrait merece una nota aparte porque es la pieza que hace esta elección menos definitiva de lo que parece: es una IR de planes relacional, agnóstica de lenguaje, que permite mover un plan entre motores sin re-parsear. DataFusion tiene productor y consumidor; Velox consume planes Substrait; Gluten serializa el plan físico de Spark a Substrait para ejecutarlo en un motor nativo. Es el mecanismo por el que «cómputo plural detrás de una cintura estrecha» deja de ser un eslogan — pero es una fase posterior, no el siguiente paso.

Lecciones de producción de Spice AI (DataFusion + Iceberg, la forma más parecida a la nuestra: aplicación + motor embebido + catálogo REST) — vale la pena leerlas porque son exactamente las trampas que nos esperan:

  1. El descubrimiento del catálogo es el cuello de botella: catálogos grandes producen arranques de 10-30 s. Lo mitigan con filtrado glob y un semáforo que capa las peticiones concurrentes a 10. (Nosotros ya lo vamos a notar: 78 + 42 tablas y subiendo.)
  2. Las firmas SigV4 caducan mientras la petición está encolada, y el síntoma aparece como error de permisos cuando en realidad es de tiempo. (Aviso directo para nuestro diagnóstico de credenciales: un 403 no siempre es un 403.)
  3. Mezcla de credenciales: OpenDAL carga credenciales de varias fuentes a la vez y falla la autenticación pese a estar bien configurada explícitamente. (Es literalmente lo que nos pasó: la secret R2 configurada y las peticiones yéndose al endpoint AWS por defecto.)
  4. Un cambio de esquema en el catálogo exige reinicio manual.

5 · Qué implica para Carbon

Inmediato (config, no arquitectura). SUPPORT_NESTED_NAMESPACES true en el ATTACH de duck-server, y pinear DuckDB y sus extensiones. Sin esto el SQL Editor lee un warehouse fantasma.

El tercer pilar propiamente dicho. La investigación converge en tres reglas, y las tres son continuaciones de lo que ya está construido, no giros:

  1. La superficie SQL es de tres niveles y la posee Index. Warehouse › Catalog › Schema › Table coincide con Databricks, Snowflake, Trino y Spice. La profundidad del namespace Iceberg es codificación física, no superficie.
  2. Las preguntas de catálogo las contesta el catálogo, no el motor — porque sólo el catálogo sabe quién pregunta. Es el patrón INFORMATION_SCHEMA de Unity Catalog, y es la misma razón por la que el motor ya está ciego para los datos. Falta cerrar el último hilo (duckColumns desde buildInfoSchemaRows).
  3. El motor recibe identidad física ya resuelta. Nunca un nombre de usuario, nunca una coordenada lógica. Es lo que la puerta ya hace con datasetFqn; el corolario es que un cambio de motor (DuckDB → DataFusion → Karma) no toca la superficie.

Lo que queda genuinamente abierto (y merece su propia iteración, no una decisión de pasillo): si el motor de lectura a medio plazo es DuckDB embebido o DataFusion + iceberg-rust —hoy DuckDB gana en DML y DataFusion en integrabilidad Rust y en hacia dónde va el ecosistema—, y si Substrait entra como IR para hacer esa elección reversible.


Fuentes