⭐⭐⭐ INGESTA GOBERNADA · FUNDAMENTOS — el contexto y el mapa
Qué es esto. El suelo sobre el que se escribe
ingesta-gobernada-approach.md. Aquí NO hay plan de
implementación: hay lo que está medido, lo que dice el estándar y los pilares
que se dan por asentados. Un approach que se apoye en memoria envejece en una semana;
uno que se apoye en esto se puede discutir.Regla de la casa: cada hecho lleva el comando o la fecha que lo demuestra.
Medido el 2026-08-16. Manda
sustrato-06sobre cualquier hecho
que aquí se afirme y allí se contradiga.
0 · El objetivo, dicho sin rodeos
Hoy la ingesta desde Postgres es un tubo hecho a mano: un worker en Railway que lee la fuente y escribe Iceberg por su cuenta. No pasa por la puerta, ni por el plan, ni por la cara. Es el último camino de escritura que no está gobernado.
El objetivo es que ingerir sea una escritura más: misma puerta, misma política, mismo motor, mismo catálogo. Y que la misma operación se pueda disparar desde el SQL Editor, desde una celda de notebook, o por un planificador — sin que ninguna de las tres tenga su propio camino.
⚠️ Esto no es federación. La decisión del 2026-08-11
(federated-catalog-wedge.md) sacó la federación del norte
de forma permanente: se COPIA, no se federa. Lo único que se federa aquí es la
metadata —para poder mirar antes de copiar—, y eso no contradice la decisión: la
contradiría consultar los datos en origen.
1 · 🗺️ EL MAPA — la infraestructura que hay, medida
SQL Editor · celda de notebook · (mañana: un planificador)
│
▼
runQuery — LA PUERTA verbos admitidos · política · plan · ledger
│
▼
Spark · GKE · sql.paladio.io el CÓMPUTO
│
│ spark.sql.catalog.carbon = org.apache.iceberg.spark.SparkCatalog
│ spark.sql.catalog.carbon.uri = https://app.paladio.io/api/iceberg
▼
LA CARA · /api/iceberg/v1 ⭐ EL ÚNICO PUNTO DE APLICACIÓN
│ ╲
│ ╲ vending de credenciales → R2 (el PLANO DE BYTES)
▼ ╲
Gravitino · catalog.paladio.io el CATÁLOGO
▲
│
Index · Postgres autoridad de NOMBRES y de POLÍTICA
⭐⭐ Lo que este mapa dice y no es obvio
- Spark no habla con el catálogo: habla con la cara. Medido —
spark.sql.catalog.carbon.uri = https://app.paladio.io/api/iceberg. Es lo que hace que la gobernanza no sea opcional: no hay ruta del motor al catálogo que no pase por ella. - La cara no es un proxy: traduce. Resuelve nombres contra Index y reenvía coordenadas físicas. Por eso preguntar a Gravitino directamente devuelve vacío y no significa que el catálogo esté vacío (trampa medida el 08-16, §4).
- La cara ya hace credential vending (
app/api/storage/credential). Es exactamente el mecanismo que el estándar de Iceberg REST define, y es la pieza que hace falta para las credenciales de la ingesta.
Estado de cada pieza
| pieza | estado | evidencia |
|---|---|---|
| Lectura por la cara | ✅ 1,8–3,5 s | e3-canary-editor-gravitino.ts VERDE |
Nacer (CREATE TABLE) | ✅ desde el 08-16 · 7,5 s | antes: 504 a los 30 s |
| CTAS | ⛔ no soportado | apache/gravitino#10750, abierta |
| Credential vending (storage) | ✅ existe | app/api/storage/credential/route.ts |
| API de gestión de Gravitino | ⛔ 404, no expuesta | GET /api/metalakes |
| Ingesta actual (el tubo) | ⛔ rota desde ~08-11 | el writer pide un warehouse por inquilino a Lakekeeper |
2 · ⛔⛔ Lo que impedía NACER — resuelto el 08-16, y por qué importa aquí
El hook createTable → importTable → loadTable de Gravitino aplana el namespace
multinivel: crea en prod.w_<hex>.t y relee en prod.t. No se encuentra a sí mismo y
devuelve 500 con la tabla ya creada.
Verificado en las dos direcciones: la tabla existe en el namespace real (200) y no en el
aplanado (404). Con un namespace de un solo nivel, createTable responde 200 en 0,8 s.
⭐ Y el 504 de 30 s del editor era el mismo fallo: un 5xx es reintentable para el
cliente Iceberg de Spark, que reintentaba con backoff hasta que el proxy cortaba. Una
causa, dos síntomas. Arreglado acuñando {env}_w_{tenant} en vez de {env}.w_{tenant}
(lib/lakehouse/opaque-namespace.ts), sin migración: el namespace se pinea por tabla.
⇒ Relevancia para la ingesta: el destino de una copia tiene que NACER. Hasta el 08-16 esto era imposible, y cualquier diseño de ingesta habría fallado en el último paso culpando a la fuente.
3 · Lo que hace la industria — y qué se toma de cada uno
Snowflake · compone primitivas, no las funde
| primitiva | qué es |
|---|---|
STAGE + FILE FORMAT | qué hay y cómo leerlo |
COPY INTO | el verbo de carga |
| Snowpipe | envuelve COPY INTO para hacerlo continuo |
| Snowpipe Streaming | sin ficheros, para alta frecuencia |
| External tables / catalog integration | eso es federación, otra cosa |
| Openflow (NiFi gestionado) | el conector Postgres: Snapshot + Incremental (CDC) |
⭐ Lo que se toma: la carga es un verbo, lo continuo lo envuelve, y el CDC no es SQL — es un objeto gestionado.
Databricks · la credencial se parte en varios securables
STORAGE CREDENTIAL (identidad cloud) · EXTERNAL LOCATION (ruta + credencial) ·
SERVICE CREDENTIAL (servicios que no son storage) · CONNECTION (sistema externo). Los
cuatro con privilegios propios.
⭐ Lo que se toma: la conexión es un securable, no una cadena dentro de un DDL. Un
CREATE TABLE … USING JDBC OPTIONS (password '…') mete el secreto en el texto de una
sentencia — es el modelo que CREATE CONNECTION vino a sustituir.
Iceberg REST · el estándar YA resuelve la credencial
El catálogo autentica, autoriza y devuelve la metadata junto con credenciales de corta
vida cuyo scope deriva de la ubicación de la tabla y de los permisos del principal
(X-Iceberg-Access-Delegation: vended-credentials, credenciales en
LoadTableResponse.config). Polaris 1.4 lo refinó en abril de 2026.
⭐ Lo que se toma: el patrón entero. Y no hay que diseñarlo — la cara ya lo hace para R2; extenderlo a otro tipo de secreto es continuidad, no invención.
4 · 🪤 Las trampas de medición que costaron esta sesión
Van aquí porque volverán en cuanto alguien mida el sustrato:
| ⛔⛔ | Preguntar a Gravitino directamente da VACÍO — {"namespaces":[]} y 404 a toda coordenada, incluida la del inquilino con 128 datasets. La cara traduce; el upstream no es la fuente. A la cara se le pregunta por la cara |
| ⛔ | El namespace multinivel se codifica con , no con un punto. Con el separador equivocado se mide otra cosa y el servidor contesta un 404 creíble |
| ⛔ | prod.w_x entre backticks es UN identificador con un punto, no dos niveles. Con la sintaxis mal, todo da 504 y parece que el camino está roto |
| ⚠️ | Una operación colgada envenena la sesión de Spark: tras un cuelgue, todo lo siguiente da 504. Medir después de un fallo mide el fallo anterior |
| ⚠️ | /api/metalakes da 404: la API de gestión de Gravitino no está expuesta. Cualquier diseño que la necesite está bloqueado antes de empezar |
5 · ⭐ LOS PILARES — lo que se da por asentado
① La conexión es un SECURABLE, no una cadena.
Vive en Index, tiene privilegios propios y entra en la cadena que recorre
decidePrivilege. Un secreto dentro del texto de un DDL no se puede revocar, ni auditar,
ni conceder a un rol.
② La credencial nunca viaja en el texto ni reside en el clúster. Se emite por petición, de corta vida y con scope, desde el punto de gobierno. Es el estándar de Iceberg REST y ya está implementado aquí para R2.
③ Un solo motor de escritura, varios disparadores.
La sentencia manual y el sync programado ejecutan lo mismo; sólo cambia quién aprieta
el botón. Es la relación exacta entre COPY INTO y Snowpipe. Es lo que permite soportar
los dos modos sin duplicar el camino.
④ Copia declarativa e incremental son cosas DISTINTAS. Un CTAS es snapshot. El CDC es estado (watermark, cursor, orden). Ninguno de los dos grandes los mete en la misma sentencia, y fingir que son lo mismo es cómo se construye un incremental que miente.
⑤ La puerta puede ser más lista que el catálogo. El catálogo no soporta CTAS. La respuesta no es renunciar ni saltárselo: es descomponer donde ya está la autoridad. La cara traduce, no proxea — ya tiene el precedente.
6 · Las decisiones que este documento NO toma
Se dejan abiertas a propósito: cerrarlas sin datos es cómo se fosiliza un error.
- ¿La conexión se puede CONSULTAR o sólo copiar? Permitir
SELECTdirecto sobrepg_ventas.…es federación de verdad, con todo lo que arrastra (empuje de predicados, latencia del origen, permisos del sistema externo). Copiar y sólo copiar es más estrecho y más defendible. - ¿El destino puede ser un catálogo/esquema arbitrario, o siempre el namespace opaco del inquilino?
- La forma del incremental. Es terreno pantanoso reconocido: hasta que no haya una copia funcionando, diseñarlo es adivinar.
7 · Comandos que sostienen este documento
# el paradigma del catálogo, por la CARA (no por el upstream)
npx dotenv -e .env.local -- npx tsx scripts/warehouse/e3-canary-editor-gravitino.ts # lectura
npx dotenv -e .env.local -- npx tsx scripts/warehouse/w6-canary-escritura-gravitino.ts # CTAS (⛔ hoy)
# el namespace opaco y su trinquete
npx vitest run lib/lakehouse/opaque-namespace.test.ts
npm run check:sin-espejo
# el estado del alta de inquilinos
npm run check:workspaces
npm run check:tenencia-derivada