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_NAMESPACES | Schemas visibles | main.default | main.test |
|---|---|---|---|
false — lo que corre hoy | bench, datasets, main, test | no existe | no existe |
true | + main.default, main.test, test.w_7b500f4d… | 78 tablas | 42 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 / plataforma | Cómo lo resolvió |
|---|---|
| Trino | Fallaba: 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) |
| Spark | SHOW 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 |
| StarRocks | Tuvo que añadir soporte multi-nivel para poder leer las tablas de Snowflake (#52451) |
| DuckDB | Lo soporta opt-in (SUPPORT_NESTED_NAMESPACES, default false); el schema anidado se direcciona citado: cat."a.b".tbl |
| Apache Iceberg 1.7.0 | El 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_schemaagrega 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
| Motor | Lectura Iceberg | Escritura | Nota |
|---|---|---|---|
DuckDB + iceberg | sí | write 1.4.0 · UPDATE/DELETE 1.4.2 · MERGE INTO, ALTER TABLE, partition transforms, V3 en 1.5.3 | Ciclo de vida completo desde un proceso embebido. Sigue etiquetado experimental; ni él ni la extensión están pineados en nuestro despliegue |
DataFusion + iceberg-rust | sí, con pushdown de predicado y de LIMIT | 0.9.0: CREATE TABLE, DROP TABLE, INSERT INTO con clustering por orden en particionadas | Sin 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 runtime | referencia | completa | El estándar de facto; el coste es operativo (cluster), no de capacidad |
| Trino | sí | sí | Distribuido, 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:
- 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.)
- 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.)
- 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.)
- 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:
- La superficie SQL es de tres niveles y la posee Index.
Warehouse › Catalog › Schema › Tablecoincide con Databricks, Snowflake, Trino y Spice. La profundidad del namespace Iceberg es codificación física, no superficie. - 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_SCHEMAde Unity Catalog, y es la misma razón por la que el motor ya está ciego para los datos. Falta cerrar el último hilo (duckColumnsdesdebuildInfoSchemaRows). - 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
- Support nested namespaces for Iceberg REST catalog · trinodb/trino#22916
- Nested namespace support is broken in 1.7.0 · apache/iceberg#11539
- spark_catalog requires a single-part namespace · apache/iceberg#4086
- Add multi-level namespaces support in Iceberg rest catalog · StarRocks#52451
- duckdb/duckdb-iceberg —
src/include/iceberg_attach.hpp,src/iceberg_attach.cpp,test/sql/local/catalog_custom_setup/fixture/nested_namespaces/ - New DuckDB-Iceberg Features in v1.5.3
- Iceberg Extension – DuckDB
- Information schema | Databricks
- Snowflake Open Catalog release notes
- Apache Iceberg at Spice AI
- Apache Iceberg Rust 0.9.0 Release
- Powered by Substrait
- Reproducción A/B del flag:
C:\tmp\nested-ns-check.py(contra Lakekeeper real, DuckDB 1.5.4)