Crítica de sustrato · DuckDB frente al estado del arte, y el salto a un motor distribuido
CRÍTICA + INVESTIGACIÓN (2026-08-05). Nada implementado. Nace de una pregunta de
producto —«¿migramos por completo el warehouse a un motor distribuido multinodo (Trino o
StarRocks), aceptando la complejidad de clusters, réplicas y k8s, porque la visión es una
solución realmente enterprise sobre estándares abiertos?»— y de una premisa que hay que
corregir antes de decidir nada: «con Trino tendríamos aislamiento de credencial S3 por
tenant nativamente». No es cierto hoy, y la corrección juega a favor — pero no por donde
parece (§0).Cruza con: duckdbengine.md (la decisión de motor vigente) ·
duck-server-tenant-warehouse-routing.md (el
enrutado que falta) · storage-vending-p4bis-approach.md
(la máquina de acuñar) · trino-openmetadata-warehouse.md
(Trino, que ya corre) · warehouse-index.md ·
carbon-sql-milestones.md · ../INFRA.md.Memoria: duckdb-engine-decision, router-junction, platform-thesis,
warehouse-narrow-waist, warehouse-storage-perimeter.
0 · La premisa que hay que corregir, y por qué es la clave de todo el documento
Trino no da aislamiento de credencial S3 por usuario final hoy. Y nosotros ya tenemos la parte buena, sin Trino.
Lo que Trino sí da nativamente (iceberg.rest-catalog.vended-credentials-enabled=true):
el motor deja de sostener una llave durable y pide credencial al catálogo por tabla,
acotada y con caducidad. Es real, es el patrón token vending machine de AWS
(storage-vending-p4bis-approach.md §1), y ya lo
tenemos corriendo y verificado — Trino 483 en om-carbon-01, grep '^s3.aws-' → 0,
SELECT en main.default ✅ / main.test ✗
(trino-openmetadata-warehouse.md §3.1).
Lo que Trino no da:
| Capacidad | Estado real, medido contra su repo |
|---|---|
| Propagar la identidad del usuario final al catálogo REST | ❌ trino#27917 abierto — «Trino does not propagate the user's OAuth2 access token»; sólo un token estático compartido |
| Credencial por usuario vía extra-credentials | 🔬 trino#27197 · trino#26320 — en desarrollo, sin cerrar |
| Que el vending funcione sin fricción | ⚠️ trino#27416 «Trino does not honour the vended credentials» (474, abierto desde nov-2025) · trino#25827 (renovación) |
Y lo decisivo: la documentación de Lakekeeper prescribe el modelo contrario al que se imagina. Su bridge OPA dice que Trino contacta al catálogo «con un usuario todopoderoso “puedo hacer de todo”» y después el motor consulta los permisos y los aplica él mismo. Trino no impersona al usuario contra el catálogo. Eso es compute enforces permissions: un modelo de confianza, no una frontera criptográfica.
⇒ Es exactamente la misma defensa de una sola capa que ya aceptamos por escrito para
duck-server (duck-server-tenant-warehouse-routing.md §5).
⭐ La tesis de este documento
Nuestro activo no es el motor: es la cara. El aislamiento no vive en quién ejecuta el
SQL, sino en dónde se acuña la llave — y esa máquina ya es nuestra:
app/api/iceberg/v1/[...path]/route.ts
lib/governance/storage-vending.ts.De ahí se siguen las dos mitades de la conclusión, y conviene leerlas juntas porque una
sola engaña:
- El motor es reemplazable ⇒ el salto es menos aterrador de lo que parece.
- El salto no arregla la gobernanza por sí solo ⇒ tampoco es la bala de plata.
1 · La crítica al stack actual, sin anestesia
1.1 · El techo no es «single node». Es mucho más bajo que eso
# services/duck-server/app/duck.py
self._lock = threading.Lock() # UNA conexión. UN lock. UNA query a la vez.
duck_lock_timeout_s: float = 20.0 # el request nº2 espera 20 s y se va con 503
duck-server sirve a TODOS los workspaces con una sola query concurrente. El límite que
nos va a matar no es «no cabe en una máquina»: es que la segunda persona que pulsa
Ejecutar recibe engine_busy. Antes de discutir multinodo hay que admitir que ni siquiera
estamos usando el mononodo — DuckDB soporta
múltiples lectores concurrentes; es
nuestro despliegue el que los serializa.
Corolario ya vivido: una conexión compartida es un radio de explosión compartido. El
comentario de _PURE_READ_VERBS documenta que un PRAGMA memory_limit='1B' colado por el
read-path degradaba a todo el mundo.
1.2 · El inventario completo de techos — y de quién es cada uno
| Techo | ¿De quién es? | ¿Lo arregla cambiar de motor? |
|---|---|---|
| 1 query concurrente | nuestra, no de DuckDB | ❌ lo arregla un cursor/pool por query |
Un warehouse fijo por proceso (ATTACH con estado) | de DuckDB | ⚠️ Trino tiene CREATE CATALOG dinámico, pero es experimental y loguea la credencial en la Web UI |
| Sin vending (llave R2 estática) | upstream (duckdb-iceberg#792/#670) | ✅ Trino sí, con las salvedades del §0 |
| Sin caché de resultados, sin MV, sin resource groups ni colas | de DuckDB | ✅ |
| Sin tolerancia a fallos en queries largas | de DuckDB | ✅ (Trino FTE / Tardigrade) |
| Merge-on-read sin compactación (F6) | nuestra | ❌ es un job; hace falta con cualquier motor |
| Un solo nodo, working set = RAM de una máquina | de DuckDB, a propósito | ✅ |
Lee la primera fila y la última juntas: el techo más urgente es nuestro y se arregla sin comprar un cluster; el techo que justifica el cluster es real pero no está medido (§1.4).
1.3 · Lo que sí hicimos bien, y conviene no romperlo
Es la mitad de la crítica que nadie escribe. Tenemos hoy lo que a Databricks le costó
años: cara Iceberg REST propia, acuñado de credencial por tabla, Index como plano de
gobernanza, y un parser/resolutor estructural en enforce. Nuestro propio
warehouse-index.md §2 clava la diferencia con Databricks: «Las suyas
están en el camino; las nuestras al lado». Un motor distribuido no mueve esa frontera ni
un milímetro — la mueve OPA/OpenFGA (§6·P4).
1.4 · Tres engaños del stack actual que hay que nombrar en voz alta
- Tenemos tres motores y ningún router de carga. Karma inerte, DuckDB sirviendo, Trino
corriendo para OpenMetadata — y Trino deliberadamente fuera de
lib/compute/engines/(trino-openmetadata-warehouse.md §2). Nuestro propio gate lo dice: «con tres motores y dos brazos, divergen» (equivalence.ts, 25 sondas). Migrar paga esa factura. - La llave «read-only» de
duck-serverno es read-only —LAKEHOUSE_S3_ACCESS_KEY_IDyLAKEHOUSE_S3_RO_ACCESS_KEY_IDson el mismo valor en Railway. El código ya prefiere la RO (duck.py, perímetro B); la credencial no existe. - No hay ni una query medida que DuckDB no aguante. Es el requisito nº 1 que nosotros mismos pusimos para abrir esta puerta. Migrar «por completo» sin ese número es cambiar un cuello de botella medido (concurrencia = 1) por uno no medido — y en el que Trino es peor: piso de cientos de ms por query frente a los ms de DuckDB.
2 · Qué tienen ellos que nosotros no — el sustrato de los tres grandes
2.1 · Snowflake — la fuerza es que no hay superficie operativa
- Multi-cluster warehouses: el aislamiento de inquilino es una unidad de cómputo, elástica y automática. Es la respuesta canónica al problema de tenencia.
- 2026 · Adaptive Compute: eliminan incluso configurar tamaño, multi-cluster o auto-suspend — se fija un objetivo de rendimiento y ellos resuelven (Summit 2026).
- Iceberg v3 más completo del mercado (deletion vectors, row lineage,
variant) + Horizon/Polaris con lectura y escritura bidireccional, y compartición a cualquier motor IRC sin cuenta de Snowflake ni egress. - Debilidad: propietario, coste, lock-in.
La lección: su fuerza no es el motor. Es que elasticidad y gobernanza son un solo
plano, con cero ops.
2.2 · Databricks — la fuerza es que la gobernanza está en el camino
- Photon: motor vectorizado en C++ bajo SQL y DataFrame.
- Unity Catalog: un catálogo para Delta + Iceberg + Parquet con un solo juego de permisos, linaje y definiciones, y el credential vending como mecanismo universal de control de acceso entre motores, construido sobre las APIs del Iceberg REST catalog (Databricks · docs).
- Serverless SQL Warehouse con Photon + Predictive IO + Intelligent Workload Management.
La lección: es literalmente el diseño que ya elegimos. UC vendiendo credenciales a
motores externos (DuckDB incluido) es nuestra cara abierta. La diferencia es madurez —
y que su motor escala.
2.3 · ClickHouse — velocidad de un propósito único, y ése no es el nuestro
- Vectorizado + SIMD + caché de resultados + scheduling de carga ⇒ imbatible en dashboards de alta concurrencia y sub-segundo.
- Debilidades que lo descalifican como sustrato de lakehouse: planificador rule-based con CBO inmaduro y sin reordenación de joins —en las comparativas publicadas por StarRocks ni terminó la carga de joins de TPC-H—; sólo lectura sobre Iceberg/Delta/Hudi (no escribe); servidores con estado y almacenamiento local que complican el resharding; queries sobre Iceberg 2-3x más lentas que sobre MergeTree nativo.
Veredicto: candidato a capa de servicio (serving), jamás a sustrato abierto.
3 · Trino vs StarRocks — el sustrato distribuido
| Trino | StarRocks | |
|---|---|---|
| Ejecución | Java · workers sin estado · shared-data | C++ vectorizado · MPP · FE/BE con estado |
| Rendimiento | correcto; SIMD limitado (Vector API experimental) | 6,93x más rápido en TPC-DS 1 TB sobre Iceberg y 16x throughput con 20 queries concurrentes ⚠️ cifras del propio vendor (StarRocks/CelerData), sintéticas — hay que re-medirlas con nuestras queries antes de apostar nada |
| Elasticidad | ⭐ sin estado ⇒ autoescala limpia en k8s (KEDA, operador Stackable) | los shared-nothing exigen replicar entre clusters (coste de almacenamiento ×2) |
| Escritura Iceberg | ✅ nativa y madura | ✅ INSERT / INSERT OVERWRITE, CREATE/DROP desde v3.1 |
| Catálogos | ⭐ HMS, Glue, Unity, Nessie, REST, JDBC + federación (PG, Mongo, Kafka…) | Iceberg/Hudi/Hive/Delta; menos amplitud |
| Seguridad fina | ⭐ OPA con row filters y column masks (desde 438) + bridge oficial de Lakekeeper | notablemente más flojo |
| Multi-cluster | ⭐ Trino Gateway (nacido en Lyft) | sin equivalente |
| Tolerancia a fallos | ⭐ FTE con exchange spooling | — |
| Punto débil | coordinador SPOF (mitigable con varios + LB), tuning de JVM/memoria, sin caché nativa | ecosistema y momentum menores; su mejor rendimiento pide su propio formato, lo que rompe la pureza open standards |
⚠️ Un dato de la industria que está desactualizado y conviene no repetir: la
comparativa de Onehouse
afirma que sólo Spark y Presto/Trino escriben formatos abiertos. StarRocks escribe
Iceberg desde la v3.1 (docs).
Veredicto para la tesis de producto («integral PERO abierto», enterprise, estándares abiertos): Trino es el sustrato; StarRocks y ClickHouse son candidatos a una capa de serving posterior, con SLA de dashboard. Tres razones, y la tercera es la que decide:
- Trino es el único que combina escritura Iceberg madura + OPA + gateway + FTE + federación.
- El bridge OPA de Lakekeeper es oficial: nuestro catálogo ya sabe hablar con Trino.
- Ya lo tenemos corriendo, y el camino de Pods/OpenMetadata ya depende de él. No es un salto: es promover a sustrato lo que ya es infraestructura.
4 · Los que ya lo hacen en vanguardia — arquitecturas reales, con números
| Quién | Forma | Números |
|---|---|---|
| Netflix (Trino Summit 2024) | 15+ clusters Trino | >10 M queries/mes, >1 M tablas Iceberg. Son los autores de Iceberg |
| Lyft | Trino para ETL, no sólo ad-hoc | ~250 K queries/día, ~10 PB/día leídos, ~100 TB/día escritos, warehouse 100+ PB en S3. Trino Gateway nació aquí (como Presto Gateway) |
| Expedia | ⭐ 3 clusters especializados: adhoc (concurrencia media, carga mixta), ETL (queries complejas, poca concurrencia), BI (simples, mucha concurrencia) | Gateway con reglas: detección de tablas grandes · select version()/show catalogs a un cluster de un solo nodo · cabecera X-Trino-Source para enrutar BI. Upgrades blue/green sin downtime |
| Shopify | Migración de PB a Iceberg con Trino | «Rewriting History», Trino Summit 2022 |
| Wise · Branch · BESTSECRET | Data lake Trino+Iceberg; autoescalado KEDA por queries encoladas y presión de memoria | — |
⭐ La lección de FORMA, que es la que hay que copiar
Nadie corre un cluster Trino. Corren clusters especializados por perfil de carga,
con un gateway delante. Lo que se compra no es «un motor distribuido»: es una flota.
Diseñar para un cluster único y descubrirlo después es el error caro.
5 · Lo que realmente cuesta el salto — y no es Kubernetes
Kubernetes es lo barato. Lo caro, en orden de sorpresa:
- Railway no da para esto. Coordinador + N workers + spooling de FTE + gateway +
autoescalado ⇒ GKE/EKS (o Dataproc/Starburst). Es un cambio de plano de
infraestructura, no un servicio más en el proyecto
cozy-comfort. - El impuesto de la equivalencia.
equivalence.tstiene 25 sondas para dos motores. Un tercero exige matriz de conformidad por motor, o divergen en silencio — y ya está escrito que ése es el fallo que no avisa. - Carbon SQL se re-mide entero. Todo el pilar M0–M6 descansa en «parser y ejecutor son
el mismo binario» (carbon-sql-milestones.md). Con Trino el
binario es otro: el pin de extensiones,
errors_as_json, la estrictez ANSI declarada ySUPPORT_NESTED_NAMESPACESse vuelven a levantar contra el dialecto de Trino — que sí soporta namespaces anidados desde la 463 connested-namespace-enabled=truey comillas obligatorias:lakehouse."main.default". - El DML del SQL Editor cambia de manos. El write-target server-side
(
DUCK_REQUIRE_WRITE_TARGET=true, hoy activo en prod) hay que reconstruirlo en Trino, donde el equivalente es OPA, no una expresión regular. - Alguien de guardia. Netflix: 15 clusters. Expedia: 3 + gateway. Eso es plantilla, no una tarde.
6 · La estrategia — no «migrar por completo», sino estratificar
La migración total de un salto es el error clásico: cambia un cuello de botella medido por uno no medido, y paga la factura de operación antes de tener el volumen que la justifica. El orden defendible es éste.
P1 · Romper la conexión única de duck-server — días, sin infraestructura nueva
Cursor por query o pool de procesos. Es el techo real y no cuesta un cluster.
De paso: emitir de una vez la llave R2 de verdad read-only (hoy _RO_ == la RW).
- Gate: dos queries concurrentes de dos workspaces distintos, ninguna con
engine_busy; yLAKEHOUSE_S3_RO_*con un token que falle al intentar escribir.
🏁 P1 · CERRADO, DESPLEGADO Y VERIFICADO EN VIVO (2026-08-05) —
ac3dad1En producción (
duck-production-ef4a…, JWT firmado, tabla real debench):
SELECT * … LIMIT 3200 · 3 filas · 10 columnas ⇒ la llave read-only llega a los bytes de R2 1 query sola 0,62 s 6 queries a la vez 0,83 s (serializadas: ~3,74 s) pool tras la tanda en_vuelo 0 · slots_perdidos 0 · cursores_libres 6·storage_credential: read_only⚠️ La lectura se comprueba con
SELECT *, no concount(*)ni con/health?deep=1:
el conteo lo contesta la metadata del snapshot y el health cuenta tablas del catálogo.
Los dos dan verde con el acceso a los bytes roto — es la misma trampa que ya costó una
sesión con Trino («listar es catálogo, leer es storage»).En frío, sobre la app ASGI real, el mismo experimento daba 6 en 3,71 s contra 4,13 s
una sola.Qué cambió: la conexión única + lock se sustituye por un pool de cursores sobre la
misma instancia, acotado por semáforo (DUCK_MAX_CONCURRENT_QUERIES, default 8). La
conexión padre conserva elATTACH, losSECRETy el contrato de dialecto, y el SQL
de usuario ya no corre en ella.Cuatro cosas que salieron de medir en vez de suponer (
services/duck-server/scripts/probe_cursor_pool.py):
interrupt()es por cursor y el cursor se reutiliza ⇒ un atasco pasa de matar
el servicio a costar 1 slot de N, y si el hilo termina tarde el slot se recupera.
wedgeddeja de ser una bandera y pasa a ser «se perdieron todos».- ⚠️
SETen el padre NO lo heredan los cursores;SET GLOBALsí. Y
duckdb_settings().scopeno sirve para saberlo: describe de dónde viene el valor en
vigor, así que diceGLOBALen conexión limpia yLOCALtras unSET. El contrato de
dialecto ahora se declara global y se verifica sobre un cursor nuevo al arrancar:
si no se hereda, el servicio no arranca.- 🔴 El pool NO arregla el radio de explosión.
memory_limites de la instancia: un
SETdesde cualquier cursor cambió el de todos (25 GiB → 190 MiB). El guard que saca
PRAGMA/SETdel read-path sigue siendo la única defensa — y ahora tiene número.- Una carrera que el pool habría abierto, cerrada de paso: el testigo de
resolver()
vivía enengine.ultimo_testigoy el router lo leía después. Con concurrencia real, la
resolución de otra petición se cuela en medio y la sombra compara contra un testigo
ajeno — sin romper nada visible, corrompiendo la cifra que gobierna el flip de M2c.
Ahora el testigo se devuelve, no se guarda.El otro techo, el invisible: los handlers llaman al motor con
asyncio.to_thread, cuyo
executor por defecto esmin(32, cpu+4)— 6 hilos en un contenedor de 2 vCPU. Subir el
semáforo a 8 y dejar ése en 6 habría mudado el cuello de botella a un sitio sin
fail-fast. Se dimensiona desde la misma cifra que el pool.Estado: 221 tests verdes (
tests/test_pool.pyfija los cinco invariantes).✅ P1b · la llave read-only — ya existe, y se probó que lo es
🔴 El paso B del perímetro se daba por hecho y no lo estaba.
LAKEHOUSE_S3_RO_ACCESS_KEY_ID
tiene en Railway el mismo valor que la read-write: la variable existía, la capacidad no,
y el log decía «perimetro B». No se detectó porque la verificación del runbook ejecutaba
facade-smoke.ts, que escribe porml-runner— medía que el escritor seguía escribiendo,
no que la llave deduck-serverhubiera dejado de poder.Lo hecho en código: el arranque compara las dos llaves y grita
PERIMETRO ABIERTO;
/healthpublicastorage_credential; y existe el control negativo
(scripts/storage/duck-ro-credential-gate.ts).Token emitido y verificado el 2026-08-05 — puesto en
.env.localy en Railway, y
pasado por el control negativo antes de confiar en él: 6 ✅ / 0 ❌.
① las dos llaves distintas ( 1cbc1f…vs34e79a…)② la RO lee LIST✅ ·GET✅ ·SELECT *en vivo ✅③ ⭐ no escribe PutObject→ AccessDenied④ ⭐ no borra DeleteObject→ AccessDeniedEl orden importó: la credencial se midió antes de ponerla en producción. «Token de
sólo lectura» es una etiqueta; que no pueda escribir es un hecho, y sólo se sabe
intentándolo.
P2 · Que duck-client.ts mande el warehouse del tenant — F-A del approach vivo
Sin esto, P1 de tenencia existe en Lakekeeper y no lo usa nadie. Ver duck-server-tenant-warehouse-routing.md §7.
- Gate: una query de una organización real viaja con su
warehouseen el body.
✅ P2 · HECHO Y DESPLEGADO (2026-08-05,
cd9791e) — y APAGADO a propósito
duck-client.tsera el único de los tres clientes Node que no resolvía el inquilino.
Ya lo manda;duck-serverlo comprueba y rechaza con 409 un warehouse que no
sirve, en vez de servir el compartido sin decirlo. Gate en vivo: 3 ✅ / 0 ❌.Pero el enrutado nace apagado (
DUCK_TENANT_WAREHOUSE_ROUTING), y el motivo está
medido: los ochotenant_warehousesestánactivey vacíos. Encenderlo hoy
dejaría al único workspace con datos (112 datasets) sin ver ninguna tabla.🔴 Lo que la sonda de P5 zanjó (
scripts/storage/p5-register-table-probe.ts)La pregunta era si mover un inquilino cuesta horas o semanas — es decir, si basta
re-registrar la tabla en el otro warehouse (metadata) o hay que copiar los bytes.
Medido con dos ubicaciones inexistentes, para no apostar 112 tablas vivas a una
lectura del contrato, y leyendo la diferencia entre los dos errores:
resultado ① registercon ubicación ajena (prefijo del compartido)400· «Provided location … is not a valid sublocation of the storage profile s3://lakehouse/test/w_7b5…»② registercon ubicación propia (control)400· «IO error … NotFound» — pasó la validación de ubicación⇒ Lakekeeper valida el prefijo ANTES de leer la metadata. Re-registrar sin mover
datos es imposible. El control es lo que lo hace concluyente: sin ②, el primer
error podía ser del fichero y no de la ubicación.La consecuencia real, y no es la que parecía: eso NO encarece a la vez las dos
salidas, las separa.
¿cuesta copiar datos? A · inquilino = warehouse (P1) — aislamiento en dos capas sí, copia real B · inquilino = primer nivel del namespace ( w_<ws>.main.default, mismo warehouse)no — la ubicación sigue siendo sublocation válida del compartido ⇒ metadata pura B da posición al inquilino sin mover un byte; el aislamiento sigue apoyado en
autorización (una capa). A da aislamiento de verdad y cuesta una copia. La sonda no
elige: acota el precio de cada una, que es lo que faltaba para poder decidir.⏰ Y una asimetría ARMADA, no disparada
El camino de escritura ya enruta al warehouse del inquilino (
ingest-client.ts,
P3); el de lectura no (P2, apagado). Medido:0 de 112datasets nacidos después
de que existiera el warehouse — porque se creó hoy a las 07:10 y el último
dataset es del 3 de agosto. El primer dataset que se cree en ese workspace nacerá
donde el SQL Editor no mira. No es un riesgo futuro: es una cuenta atrás.
🏁 P2 · CERRADO (2026-08-05) — con la decisión de mapeo tomada
La decisión: un catálogo = un warehouse. Un schema = un namespace de UN nivel.
El error no era dónde están las tablas: era el nivel al que proyectamos. Metíamos
el catálogo dentro del namespace (main.default) y dejábamos el prefijo REST sin
usar. Los tres referentes coinciden en lo contrario:
Nivel Databricks UC Snowflake Open Catalog Lakekeeper Carbon (destino) unidad de aislamiento, con storage propio catálogo catálogo warehouse = {prefix}catalogsde Index → un warehouseagrupación lógica schema namespace namespace schemasde Index → namespace de 1 niveldato tabla tabla tabla tabla Fuentes, y son prescriptivas, no interpretación:
«catalogs should be used as the primary unit of isolation» · «assign managed
storage at the catalog level for logical data isolation» · «Bind specific
catalogs to specific workspaces»
(UC best practices ·
managed storage).
UC lo expone tal cual en su cara Iceberg:
/v1/catalogs/<uc_catalog>/namespaces/<uc_schema>/tables/<t>— el catálogo ES el
prefijo. Y AWS lo dice desde el otro lado: el patrón pool es «un prefijo por
inquilino con la credencial fijada a él», que es exactamente elkey-prefixde un
warehouse; el bridge (silo para quien lo exija, pool para el resto) es lo que ya
reserva §5 destorage-tenancy-approach.mdpara residencia o bucket del cliente.La transformación, que es mecánica
HOY warehouse `lakehouse` · namespace `main.default` · ds_<uuid> DESTINO warehouse del catálogo · namespace `default` · ds_<uuid> ▲ ▲ el `main` SUBE aquí y desaparece de aquíEl primer nivel del namespace no se borra: asciende al prefijo. Por eso el
destino no inventa nada — reordena lo que ya existe.⚠️ Un matiz que hay que dejar escrito ahora para no pagarlo luego: P1 nombró los
warehouses por workspace (carbon-<env>-w-<ws>), y la regla correcta es por
catálogo. Hoy coinciden —cada workspace tiene exactamente un catálogo,main,
medido— así que los warehouses de P1 sirven tal cual. El día que un workspace quiera
un segundo catálogo, ese catálogo necesita su propio warehouse, y el nombre
tendrá que llevar el catálogo, no el workspace. No se renombra hoy (renombrar un
warehouse es recrearlo); se registra la deuda.Qué queda cerrado y qué no
✅ P2 (el enrutado) está hecho, desplegado y verificado —
cd9791e, gate 3/0.
El campo viaja,duck-serverrechaza con 409 lo que no sirve, y la palanca
DUCK_TENANT_WAREHOUSE_ROUTINGespera.🔜 Lo que falta es P5, y ya no es una incógnita: es un trabajo acotado. La sonda
midió que exige copia, y el volumen resultó ser 75,5 MB en 1.680 objetos — la
tabla mayor, 13,9 MB. La superficie real son 92 tablas, no 112: los 20 «sin
namespace» (18 vacíos) no están materializados en Iceberg y quedan fuera por
medición, no por borrado. Tramos:main.test(14, las activas) como canario,
luegomain.default(78, ninguna tocada en 30 días).Y no se recortan las tablas para abaratar el canario. El coste de esta migración
no son los bytes —son segundos— sino la superficie de corrección: los__row_index
legados, eldefault-sort-order-idque bloquea escrituras en las pre-D·4, las 5
vacías. Una migración validada sobre 3 tablas elegidas a mano es el fallo que ya
tenemos escrito: un instrumento que mide una copia no mide el sistema.
P3 · Promover Trino a motor de Carbon — lib/compute/engines/trino.ts, detrás de Junction
🔄 EL GATE ESTABA MAL FORMADO (corregido 2026-08-05)
El gate original era «una consulta MEDIDA que DuckDB no aguante». Mide hoy, no el
objetivo de diseño, y por eso está del revés: el volumen actual —75,5 MB— es una
decisión de producto, no una restricción. El sustrato tiene que estar
físicamente preparado para concurrencia masiva, al nivel de Snowflake o
ClickHouse, antes de que llegue la carga. Un gate que sólo dispara cuando ya duele
es reactivo; para una plataforma que se vende como enterprise desde el sustrato, no
sirve.Se sustituye por los gates de trino-substrate-approach.md §5:
alcanzarlo por la vía correcta, identidad y política, y el tercer brazo del
comparador. Ninguno depende de cuántos datos haya hoy.Lo que sí se conserva del razonamiento anterior es que la entrada de Trino no se
justifica por rendimiento, y hay que decirlo para no vender lo que no se ha medido —
«capability-check por fe» es exactamente lo que prohíbejunction.md§9 principio 12
(no nombrar capas que aún están huecas) y principio 11
(harness-diferencial-antes-de-servir).Lo que sí apareció, midiendo P1 y P2, son dos límites estructurales que no se
arreglan esperando a tener más datos:
El límite Por qué Trino ① Tenencia ATTACHes de la instancia de DuckDB, no de la conexión (medido en la sonda del pool). El aliaslakeno puede significar dos warehouses a la vez ⇒ un duck-server no puede servir a N inquilinos con warehouse propio — justo a donde va el sustrato tras P2monta N catálogos en un proceso; es su modelo, no un apaño ② Gobernanza duck-server se ata a Lakekeeper y se salta la cara ( app/api/iceberg/v1)Trino ya apunta a /api/iceberg⇒ hereda grants,access_eventsy políticas sin configurar nada. Se nota: con los grants actuales ve 91 tablas y no 112, porquemain.testse dejó sin conceder como control negativo⇒ Trino es hoy el motor MÁS GOBERNADO que tenemos, no el más rápido. Ése es el
argumento de sustrato; el de rendimiento vendrá o no vendrá.✅ Registrado e INERTE (
lib/compute/engines/trino.ts)Mismo criterio que Karma:
availablees un eje separado desql/read, y un
motor no desplegado no puede mentir sobre sí mismo. Trino no publica ningún
puerto —hoy sólo le habla la ingesta de OpenMetadata por la red interna de Docker—
así que sinTRINO_URLdeclaraavailable: falsecon el motivo,getSqlEngine
lanzaEngineUnavailableError(despliegue, no capacidad) yavailableSqlEngines()
no lo incluye.write: falsea propósito: el escritor del Warehouse es uno y sólo
uno, y la ACLread-onlyes una de las tres capas que protegen esa invariante.⛔ Y el Trino de la VM de OpenMetadata NO es el camino
Es un
docker-composesin puerto publicado, sin autenticación, sin TLS y sin
aislamiento, montado para que el perfilado de OM tuviera números. Cumplió su función
y es legacy. Absorberlo como motor principal desde ahí sería heredar sus
limitaciones y llamarlo arquitectura.La vía correcta está en trino-substrate-approach.md:
operador en Kubernetes (TrinoCluster+TrinoCatalogcomo CRDs — que es justo lo
que hace declarativo «un catálogo por warehouse de inquilino»), Trino Gateway
delante, TLS + shared secret + OIDC contra el Keycloak que ya tenemos, OPA para
row-filters y column-masks (con el bridge oficial de Lakekeeper), FTE con exchange
manager y autoescalado por cola. Fases T1-T5 con sus gates.
P4 · Mover la gobernanza al camino, también para Trino
Bridge OPA + OpenFGA de Lakekeeper ⇒ row filters y column masks aplicados por el motor. Es lo que hace Unity Catalog, y es lo que cierra la brecha de warehouse-index.md §2 («las nuestras están al lado»).
- Gate: el mismo control negativo que P1-P3 de storage: con el motor sirviendo a la organización A, un intento explícito contra B falla en el motor, no por ausencia de referencia.
P5 · Cuando duela, la flota
Trino Gateway + clusters por perfil (interactivo / ETL / BI), copiando el modelo de Expedia. Aquí —y no antes— entra k8s.
- Gate: una regla de enrutado en producción y un upgrade blue/green sin downtime.
P6 · La cara abierta no se toca nunca
Siga quien siga siendo el motor, la llave se acuña en un solo sitio. Es lo único que da el aislamiento que Trino no va a dar solo (§0).
- Gate:
npm run check:storage-perimetersigue en verde con el motor nuevo dentro.
7 · El punto que cierra el círculo
La visión —enterprise, abierta, vanguardista— no la entrega el motor distribuido.
La entregan la cara Iceberg REST, el Index y el vending, que ya existen y ya funcionan.
El motor distribuido entrega escala y concurrencia — que es un problema real, medible,
y que hoy tenemos en1.
Separar esas dos frases es lo que evita las dos formas de equivocarse a la vez: creer que un cluster nos vuelve enterprise (no lo hace: la gobernanza es nuestra y ya está construida), y creer que por tener la gobernanza podemos ignorar el motor (tampoco: una query concurrente no sirve a nadie).
8 · Lo que este documento NO decide
- El número del gate P3 — qué query, con qué volumen y con qué latencia objetivo declara a DuckDB insuficiente. Es una medición pendiente, no una opinión.
- Trino vs StarRocks en firme — se recomienda Trino (§3), pero la comparativa de rendimiento disponible es del vendor; el desempate honesto es re-medir con nuestras queries sobre nuestras tablas.
- Dónde vive el cluster (GKE/EKS/Dataproc/Starburst) y su coste — §5·1 dice que Railway no llega; no dice a dónde se va.
- El destino de Karma. Sigue siendo el acelerador de lectura por índice (karma-engine-repo); un sustrato distribuido cambia el cálculo de su valor, y esa re-evaluación merece su propio documento.
Fuentes
Trino: metastores / REST catalog · catálogos dinámicos · OPA access control · fault-tolerant execution · Trino Gateway · #27917 · #27416 · #27197 · #26320 · #25827
Implementaciones: Expedia · operating Trino at scale · Trino Summit 2024 (Netflix, Wise, Branch) · Lyft · large-scale ETL · Shopify · Rewriting History · BESTSECRET · KEDA
Catálogo: Lakekeeper OPA bridge · Lakekeeper query engines
Los tres grandes: Databricks · Completing the Lakehouse Vision · UC credential vending · SQL warehouse types · Snowflake Summit 2026 · ClickHouse is data lake ready
Comparativas: Onehouse · motores analíticos · CelerData · Trino vs StarRocks · StarRocks · Iceberg catalog · DuckDB · concurrencia · Rill · DuckDB won by refusing to scale out
Documento de crítica y decisión. Si algún punto de §6 se ejecuta, este doc se actualiza con el resultado medido — no con la intención.