Published

Crítica de sustrato · DuckDB frente al estado del arte, y el salto a un motor distribuido

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...

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 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:

CapacidadEstado real, medido contra su repo
Propagar la identidad del usuario final al catálogo RESTtrino#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:

  1. El motor es reemplazable ⇒ el salto es menos aterrador de lo que parece.
  2. 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 concurrentenuestra, 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 colasde DuckDB
Sin tolerancia a fallos en queries largasde 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áquinade 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

  1. 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.
  2. La llave «read-only» de duck-server no es read-onlyLAKEHOUSE_S3_ACCESS_KEY_ID y LAKEHOUSE_S3_RO_ACCESS_KEY_ID son el mismo valor en Railway. El código ya prefiere la RO (duck.py, perímetro B); la credencial no existe.
  3. 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

TrinoStarRocks
EjecuciónJava · workers sin estado · shared-dataC++ vectorizado · MPP · FE/BE con estado
Rendimientocorrecto; 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 maduraINSERT / 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 finaOPA con row filters y column masks (desde 438) + bridge oficial de Lakekeepernotablemente más flojo
Multi-clusterTrino Gateway (nacido en Lyft)sin equivalente
Tolerancia a fallos⭐ FTE con exchange spooling
Punto débilcoordinador SPOF (mitigable con varios + LB), tuning de JVM/memoria, sin caché nativaecosistema 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:

  1. Trino es el único que combina escritura Iceberg madura + OPA + gateway + FTE + federación.
  2. El bridge OPA de Lakekeeper es oficial: nuestro catálogo ya sabe hablar con Trino.
  3. 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énFormaNúmeros
Netflix (Trino Summit 2024)15+ clusters Trino>10 M queries/mes, >1 M tablas Iceberg. Son los autores de Iceberg
LyftTrino 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)
Expedia3 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
ShopifyMigración de PB a Iceberg con Trino«Rewriting History», Trino Summit 2022
Wise · Branch · BESTSECRETData 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:

  1. 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.
  2. El impuesto de la equivalencia. equivalence.ts tiene 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.
  3. 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 y SUPPORT_NESTED_NAMESPACES se vuelven a levantar contra el dialecto de Trino — que sí soporta namespaces anidados desde la 463 con nested-namespace-enabled=true y comillas obligatorias: lakehouse."main.default".
  4. 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.
  5. 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-serverdí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; y LAKEHOUSE_S3_RO_* con un token que falle al intentar escribir.

🏁 P1 · CERRADO, DESPLEGADO Y VERIFICADO EN VIVO (2026-08-05) — ac3dad1

En producción (duck-production-ef4a…, JWT firmado, tabla real de bench):

SELECT * … LIMIT 3200 · 3 filas · 10 columnas ⇒ la llave read-only llega a los bytes de R2
1 query sola0,62 s
6 queries a la vez0,83 s (serializadas: ~3,74 s)
pool tras la tandaen_vuelo 0 · slots_perdidos 0 · cursores_libres 6 · storage_credential: read_only

⚠️ La lectura se comprueba con SELECT *, no con count(*) 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 el ATTACH, los SECRET y 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):

  1. 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.
    wedged deja de ser una bandera y pasa a ser «se perdieron todos».
  2. ⚠️ SET en el padre NO lo heredan los cursores; SET GLOBAL sí. Y
    duckdb_settings().scope no sirve para saberlo: describe de dónde viene el valor en
    vigor, así que dice GLOBAL en conexión limpia y LOCAL tras un SET. 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.
  3. 🔴 El pool NO arregla el radio de explosión. memory_limit es de la instancia: un
    SET desde cualquier cursor cambió el de todos (25 GiB → 190 MiB). El guard que saca
    PRAGMA/SET del read-path sigue siendo la única defensa — y ahora tiene número.
  4. Una carrera que el pool habría abierto, cerrada de paso: el testigo de resolver()
    vivía en engine.ultimo_testigo y 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 es min(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.py fija 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 por ml-runner
— medía que el escritor seguía escribiendo,
no que la llave de duck-server hubiera dejado de poder.

Lo hecho en código: el arranque compara las dos llaves y grita PERIMETRO ABIERTO;
/health publica storage_credential; y existe el control negativo
(scripts/storage/duck-ro-credential-gate.ts).

Token emitido y verificado el 2026-08-05 — puesto en .env.local y en Railway, y
pasado por el control negativo antes de confiar en él: 6 ✅ / 0 ❌.

① las dos llavesdistintas (1cbc1f… vs 34e79a…)
② la RO leeLIST ✅ · GET ✅ · SELECT * en vivo ✅
③ ⭐ no escribePutObjectAccessDenied
④ ⭐ no borraDeleteObjectAccessDenied

El 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 tenantF-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 warehouse en el body.

✅ P2 · HECHO Y DESPLEGADO (2026-08-05, cd9791e) — y APAGADO a propósito

duck-client.ts era el único de los tres clientes Node que no resolvía el inquilino.
Ya lo manda; duck-server lo 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 ocho tenant_warehouses están active y 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
register con ubicación ajena (prefijo del compartido)400 · «Provided location … is not a valid sublocation of the storage profile s3://lakehouse/test/w_7b5…»
register con 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, 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 112 datasets 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:

NivelDatabricks UCSnowflake Open CatalogLakekeeperCarbon (destino)
unidad de aislamiento, con storage propiocatálogocatálogowarehouse = {prefix}catalogs de Index → un warehouse
agrupación lógicaschemanamespacenamespaceschemas de Index → namespace de 1 nivel
datotablatablatablatabla

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 el key-prefix de un
warehouse; el bridge (silo para quien lo exija, pool para el resto) es lo que ya
reserva §5 de storage-tenancy-approach.md para 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 verificadocd9791e, gate 3/0.
El campo viaja, duck-server rechaza con 409 lo que no sirve, y la palanca
DUCK_TENANT_WAREHOUSE_ROUTING espera.

🔜 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,
luego main.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, el default-sort-order-id que 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 Carbonlib/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 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íbe junction.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ímitePor qué Trino
TenenciaATTACH es de la instancia de DuckDB, no de la conexión (medido en la sonda del pool). El alias lake no 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
Gobernanzaduck-server se ata a Lakekeeper y se salta la cara (app/api/iceberg/v1)Trino ya apunta a /api/iceberg ⇒ hereda grants, access_events y políticas sin configurar nada. Se nota: con los grants actuales ve 91 tablas y no 112, porque main.test se 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: available es un eje separado de sql/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 sin TRINO_URL declara available: false con el motivo, getSqlEngine
lanza EngineUnavailableError (despliegue, no capacidad) y availableSqlEngines()
no lo incluye. write: false a propósito: el escritor del Warehouse es uno y sólo
uno, y la ACL read-only es una de las tres capas que protegen esa invariante.

⛔ Y el Trino de la VM de OpenMetadata NO es el camino

Es un docker-compose sin 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 + TrinoCatalog como 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-perimeter sigue 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 en 1.

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.