RUBIX FEDERATION · EL ESPEJO — cómo lo resuelven los punteros, y qué se deriva
⚠️ IDEA INICIAL — no es el estado vigente de Rubix.
Este documento es la investigacion del 17 de agosto de 2026 de la que salio Rubix
como producto y como repositorio aparte (describeloai/Rubix, cuyos docs fundacionales
se escribieron hora y media despues). No se ha promovido al repo nuevo a proposito:
alli seria ruido — lo que aqui se explora, alli ya esta decidido. Se conserva en Carbon
como registro del ORIGEN, y porque las consecuencias que deriva para Carbon siguen
gobernando esta linea.
Para qué es esto. FD·2 se bloqueó en una pregunta que parecía de fontanería —¿dónde
aterriza el espejo, si el régimen N·4 es para tablas con identidad física?— y resultó
ser la pregunta de arquitectura del producto de federación. Esto es la investigación
de las cuatro arquitecturas paralelas y lo que se deriva para Carbon.Regla de la casa: cada hecho lleva su fuente.
0 · ⭐⭐⭐ LA TESIS, primero — porque reordena todo lo demás
El espejo es AUTORIDAD DE GOBIERNO, nunca AUTORIDAD DE DATOS.
Del espejo salen los nombres, los permisos, el linaje y la búsqueda.
Del origen salen siempre, en vivo, las filas y los tipos con los que se ejecuta.
Esto no es una preferencia: es la única postura desde la que un espejo rancio no puede
dar un resultado incorrecto. Y es donde ya estamos sin habérnoslo propuesto — F·1 y O·2
resuelven <conexión>.<esq>.<tabla> contra el origen por la vista JDBC, medido, con
pushdown y JOIN mixto. Ya federamos sin espejo.
⇒ El espejo no se construye para poder consultar. Se construye para poder gobernar, navegar, linear y buscar. Esa frase decide el resto del documento.
1 · Los cuatro modelos, y qué le pasó a cada uno
① Trino / Starburst — sin espejo, caché opcional en memoria
No hay catálogo persistente de objetos foráneos: el conector le pregunta al origen en cada
planificación. Hay caché, y es opt-in: metadata.cache-ttl viene en 0ms, o sea
desactivada, con metadata.cache-missing y system.flush_metadata_cache() para vaciarla.
⛔⛔ Y hay evidencia dura de por qué ese default es 0: el issue
trinodb/trino#10512 — «Metadata caching
may return incorrect data». La caché de Guava tenía una carrera entre la carga en vuelo
y la invalidación: invalidar una clave no cancelaba la carga en curso, que acababa
insertando un valor ya rancio. Afectaba a CachingJdbcClient y a CachingHiveMetastore.
Arreglado en Trino 369.
⭐ La lección, que es la que importa: una caché de metadata mal invalidada no falla — miente. Devuelve resultados incorrectos, no errores.
② Databricks — espejo poblado por crawl, refrescado en tiempo de consulta
Unity Catalog «crawls the external catalog to populate a foreign catalog», y a partir de ahí UC es la capa de gobierno: comprueba acceso e infiere linaje, mientras «the query itself runs against the underlying external catalog».
El refresco es automático en tiempo de consulta: si el esquema del origen cambió, UC
trae la última metadata cuando la query corre. El REFRESH FOREIGN {CATALOG|SCHEMA|TABLE}
manual se recomienda en dos casos, y los dos son reveladores:
- consistencia para motores externos — «paths that bypass Databricks Runtime don't trigger automatic refreshes, which can result in stale metadata»;
- rendimiento — porque si no, «the first query otherwise triggers a full refresh».
⭐⭐⭐ Y el hallazgo de seguridad, que es lo más valioso de toda la investigación: los
AUTHORIZED PATHS. Con federación de un Hive metastore inseguro, un usuario que pueda
cambiar el path de una tabla en el HMS consigue que en el siguiente refresco UC actualice
el path de la tabla federada. Como UC ya tiene credencial de storage sobre ese bucket y
el usuario tiene SELECT sobre esa tabla, acaba leyendo datos a los que no tenía
acceso. La mitigación son los authorized paths: un guardarraíl sobre qué rutas puede
cubrir la federación.
⇒ Un espejo que copia del origen le está delegando autoridad al origen. Si el origen es manipulable, el espejo es un vector de escalada.
⚠️ Y Databricks separa dos cosas que aquí conviene no mezclar: query federation (JDBC, ejecuta el origen) y catalog federation (acceso directo a los ficheros en object storage, ejecuta el compute de Databricks). El ataque de arriba es del segundo modo.
③ Dremio — espejo con políticas de caducidad, y la query se reinicia
Cachea esquema, layout de particiones y listas de ficheros, sobre dos ejes distintos:
| eje | qué es |
|---|---|
| Dataset Discovery | los NOMBRES de los objetos de arriba: bases, tablas |
| Dataset Details | lo que hace falta para PLANIFICAR: campos, tipos, shards, estadísticas, localidad |
Y dos modos de refresco: inline —si al lanzar la query la metadata expiró, la query se
pausa hasta que termina— y background programado, que es lo que se recomienda para no
pagar el inline. Manual: ALTER TABLE … REFRESH METADATA.
⭐ Y la frase que responde a una pregunta que teníamos abierta: si durante la ejecución Dremio descubre que la metadata expiró o quedó inválida, «a metadata refresh is triggered and the query restarts after the refresh». La query se reinicia. Ésa es la respuesta honesta a «qué pasa si el espejo y el origen discrepan a mitad de una consulta» cuando el espejo sí es autoridad de planificación: no hay arreglo elegante, hay reintento.
④ Denodo — espejo explícito e importado, con reconciliación asistida
Las base views se importan del origen y quedan en el catálogo del servidor como objetos de primera clase. El Source Refresh compara la vista guardada con el origen y clasifica cada campo en cuatro estados:
| 🟢 verde | campos nuevos |
| ⚪ gris | campos eliminados |
| 🔴 rojo | cambios observados en el ORIGEN que pueden afectar a la vista |
| 🟠 naranja | cambios hechos por el USUARIO — la vista no coincide con el origen pero no se observó cambio en el origen: la diferencia la introdujo quien alteró la vista |
Las altas y bajas se guardan siempre, y los cambios se propagan a las vistas dependientes, mostrando antes la lista de las afectadas.
⭐⭐⭐ La joya es la distinción rojo/naranja. Separa «cambió el origen» de «lo cambié yo». Un espejo que no las distingue, al refrescar, pisa el trabajo del usuario en silencio — y eso no se nota hasta que alguien busca la anotación que ya no está.
2 · ⭐⭐ Dónde estamos nosotros, que no es donde ninguno de los cuatro
| espejo | quién resuelve los DATOS | si el espejo está rancio | |
|---|---|---|---|
| Trino | no (caché opt-in) | el origen | ⛔ resultados incorrectos (#10512) |
| Databricks | sí, crawl | el origen (query fed.) | refresco en tiempo de consulta |
| Dremio | sí, con TTL | el origen / los ficheros | la query se reinicia |
| Denodo | sí, importado | el origen | reconciliación asistida |
| Carbon hoy | no | el origen, por la vista JDBC | (no hay espejo) |
Ya federamos sin espejo, y funciona — F·1 y O·2, medidos: zero-copy, pushdown, JOIN mixto, navegación. Eso significa que llegamos a esta decisión desde el lado bueno: los otros construyeron el espejo porque su planificador lo necesitaba; nosotros no lo necesitamos para consultar.
⇒ Podemos permitirnos la postura que ellos no pueden: mantener el espejo FUERA del camino de la lectura. Y de ahí sale, gratis, la propiedad que a Trino le costó un bug de resultados incorrectos: un espejo rancio nunca da un resultado malo, sólo un catálogo desactualizado.
⚠️ Y hay que declararlo como invariante, no dejarlo como casualidad. La tentación de
«ya que tenemos las columnas espejadas, ahorrémonos el DESCRIBE» aparecerá el día que
alguien mire la latencia — y ese día el espejo se convierte en autoridad de datos y
heredamos #10512 entero.
3 · Lo que se DERIVA para Rubix
⓪ El invariante, con trinquete
RUBIX·I1 — El espejo no participa en la resolución de datos.
Ninguna lectura federada toma columnas, tipos ni coordenadas de ejecución del espejo. El
espejo dice dónde está y si puedes; el origen dice qué hay.
Es el mismo tipo de propiedad que FD·6 (que el motor no vea el catálogo) y se protege
igual: con un trinquete, porque un comentario no para una optimización razonable.
① Dónde vive — resuelto, y por un motivo mejor que el del trinquete
Tablas propias colgando de foreign_databases: foreign_schemas · foreign_tables ·
foreign_columns. No datasets, no el namespace de N·4.
El argumento de partida era «una tabla foránea no tiene identidad física y el trinquete de
N·4 la juzgaría mal». Cierto, pero secundario. El de verdad es I1: datasets es la
autoridad de datos del Warehouse — de ahí sale la resolución hacia el catálogo. Meter el
espejo ahí lo convertiría en autoridad de datos por construcción, que es exactamente lo
que el invariante prohíbe.
⭐ Y de paso desaparece la tentación del namespace nulo: en datasets, iceberg_namespace
nulo significa «nunca se materializó», y una tabla foránea es lo contrario — existe de
verdad, sólo que fuera. Habría sido una fila que pasa el trinquete diciendo algo falso.
② Los dos ejes del refresco — de Dremio
REFRESH FOREIGN { DATABASE | SCHEMA | TABLE }, la superficie de Databricks con nuestro
nombre, sobre los dos ejes que Dremio separa:
- descubrimiento (qué esquemas y tablas hay) — barato: FD·0 lo midió en 48 tablas / 2 esquemas en 4,1 s con una sola vista JDBC;
- detalle (columnas y tipos) — por tabla, y es lo que hay que poder pedir en grano fino.
⛔ Sin refresco inline y sin refresco en tiempo de consulta, y ésa es la decisión que nos separa de Databricks y de Dremio. Ellos los necesitan porque su espejo es autoridad de planificación. Con I1 no hacen falta: la consulta no lo consulta. Nos ahorramos el «la primera query dispara un refresco completo» y el «la query se reinicia».
③ El estado por objeto — de Denodo, entero
Cada objeto espejado lleva su estado de reconciliación, y los cuatro hacen falta:
| estado | qué significa | qué hace el refresco |
|---|---|---|
nuevo | está en el origen y no en el espejo | lo añade |
retirado | estaba y ya no | lo marca sin borrarlo — el linaje de lo que se copió de él sigue vivo |
cambiado-en-origen | el origen lo alteró | lo actualiza y lo señala |
divergente | difiere y el origen no cambió ⇒ lo cambió alguien aquí | ⛔ NO lo pisa |
El cuarto es el que casi nadie implementa y el que evita el fallo silencioso: en cuanto una tabla foránea acumule estado gobernado —etiquetas, políticas, permisos por columna—, un refresco que sobrescriba sin mirar borra trabajo sin decirlo.
④ ⛔⛔ El guardarraíl equivalente a los authorized paths
FD·3 va a autorizar la lectura con TABLE_READ_DATA sobre una tabla cuya definición vino
del origen. Ése es literalmente el escenario del ataque de Databricks: si el origen puede
redefinir a qué apunta esa tabla, un permiso concedido sobre «pedidos» acaba dando otra cosa.
⭐ Nuestra posición es mejor, y conviene entender por qué: nuestro perímetro es la credencial JDBC del cliente, no una credencial de storage nuestra. Lo que el espejo puede alcanzar está acotado por lo que esa credencial ya podía leer, así que una redefinición no escala fuera de la conexión. El problema se reduce a «concedí sobre A y ahora A significa B, dentro de la misma base» — real, pero contenido.
⇒ El guardarraíl que sí hace falta: la coordenada remota de una tabla foránea es inmutable
tras crearse. Un refresco que la vea cambiada no la actualiza en silencio: la marca
cambiado-en-origen y deja que alguien lo mire. Es el 🔴 de Denodo puesto al servicio de
lo que Databricks aprendió por las malas.
⛔⛔ Y la advertencia con fecha: el día que Rubix haga catalog federation al estilo Databricks —leer ficheros del cliente en object storage con nuestra credencial— heredamos el problema de los authorized paths entero, porque el perímetro deja de ser la credencial del cliente. Ese día hace falta el guardarraíl de rutas antes de la primera consulta, no después.
4 · Qué le pasa a FD·2 con esto
| pregunta abierta (approach §7) | estado |
|---|---|
| ① ¿dónde vive el espejo en N·4? | 🏁 resuelta — fuera de datasets y fuera de N·4, por I1 |
| ③ la caducidad del espejo | 🏁 resuelta — no hay refresco inline ni en tiempo de consulta; el espejo no está en el camino de la lectura |
| ③·bis ¿y si discrepan a mitad de una consulta? | 🏁 disuelta — la consulta no lee del espejo, así que no puede discrepar con él |
② la compatibilidad de <conexión>.<esq>.<tabla> | sigue abierta |
| ⑤ los tipos que el driver no sepa representar | sigue abierta, y ahora es sólo del espejo: la ejecución no depende de él |
⇒ FD·2 se puede escribir. Y es más pequeño de lo que parecía, porque las dos partes caras —el refresco inline y la coherencia bajo consulta— desaparecen con el invariante en vez de resolverse.
5 · 📎 Fuentes
caché de metadata desactivada por defecto, flush_metadata_cache() | Trino · PostgreSQL connector |
| «Metadata caching may return incorrect data» · carrera load-in-flight vs invalidación | trinodb/trino#10512 |
| UC crawls el catálogo externo · refresco en tiempo de consulta · authorized paths y el ataque | Databricks · What is catalog federation? |
REFRESH FOREIGN { CATALOG | SCHEMA | TABLE } | Databricks · REFRESH FOREIGN |
| Dataset Discovery vs Dataset Details · inline vs background · la query se reinicia | Dremio · Refreshing Metadata · Optimize Metadata Refresh Frequency |
| Source Refresh · los cuatro estados (verde/gris/rojo/naranja) · propagación a vistas dependientes | Denodo · Source Refresh |
USE CONNECTION, CREATE FOREIGN CATALOG, y que leer se gobierna con SELECT | Unity Catalog privileges reference |
SQL/MED — SERVER, USER MAPPING, IMPORT FOREIGN SCHEMA | ISO/IEC 9075-9 · PostgreSQL · Foreign Data |
| zero-copy, pushdown y JOIN mixto medidos aquí | f1-gate-federacion.ts · o1-inventario-operaciones.ts |