FD · LA BASE FORÁNEA — approach de la entrega
Dónde encaja. Es el siguiente paso de la iteración de orígenes de datos, no una
entrega nueva al lado. Continúa la serie C·2 → C·3 → C·4 → C·5 → C·6 → F·1 → W·1 → O·1
(ingesta-gobernada-approach.md,
f1-federacion-por-la-cara.md,
handoff-2026-08-17-federacion-y-corte-del-tubo.md)
y no inventa sustrato: el sustrato ya está construido y medido. Esto le pone
encima la entidad que le falta.Regla de la casa: cada hecho lleva el comando que lo demuestra.
0 · ⭐⭐⭐ La frase que manda sobre esta entrega
Un origen no se consulta a través de una conexión: se consulta porque es una BASE
que está en el catálogo.
Hoy SELECT * FROM <conexión>.<esquema>.<tabla> exige CONNECTION_USE — el permiso
de usar la conexión. Eso significa que la lectura de datos se autoriza contra el objeto
equivocado, y no es una preferencia estética: es lo que hace que la pregunta «¿quién
puede leer esta tabla de fuera?» no tenga respuesta expresable en el plano de
permisos. Se puede decir «éste puede usar la conexión» y nada más fino.
⇒ Lo que falta no es un privilegio: es un OBJETO. La FOREIGN DATABASE.
1 · El estándar del mercado, y dónde estamos ya dentro de él
SQL/MED (ISO/IEC 9075-9, en SQL:2003) es la parte del estándar que gobierna esto, y
descompone el problema en cinco piezas. Puestas al lado de lo que ya existe aquí:
| SQL/MED (ISO) | Databricks | Snowflake | Carbon, hoy |
|---|---|---|---|
FOREIGN DATA WRAPPER — el adaptador | (implícito en el tipo) | — | 🏁 TYPE POSTGRESQL de CREATE CONNECTION (C·2) |
SERVER — dónde vive el origen | CONNECTION | — | 🏁 connections (C·2), securable con cadena [workspace, connection] |
USER MAPPING — con qué credencial | credencial de la conexión | — | 🏁 vending por PERSONA con TTL (C·3) + CarbonJdbcConnectionProvider |
FOREIGN TABLE — la tabla, consultable | tabla del foreign catalog | tabla | ⛔ no existe |
IMPORT FOREIGN SCHEMA — traerse las definiciones | REFRESH FOREIGN … | — | ⛔ no existe |
| (el contenedor de arriba) | FOREIGN CATALOG | DATABASE | ⛔ no existe ← lo que abre esta entrega |
⭐ Tres de las cinco piezas están hechas, medidas y en producción. Esta entrega es la mitad de arriba, no un rediseño.
La forma que se adopta, y por qué
FOREIGN DATABASE, con namespacing database.schema.object — el corte de Snowflake.
No FOREIGN CATALOG: en Carbon catálogo ya nombra al plano de gobierno entero (Index,
la cara, el régimen N·4), así que reutilizar la palabra para «el espejo de la base de un
cliente» haría que catalog significara dos cosas en la misma frase. database es
exactamente lo que el cliente tiene delante en su sistema, y el usuario que escribe
ventas.public.pedidos está escribiendo el nombre que ya conoce.
FOREIGN DATABASE ventas › SCHEMA public › TABLE pedidos
el espejo de UNA base el del origen la tabla foránea
(securable, gobernable) (securable) (securable ⇒ SELECT)
2 · ⭐⭐ Lo que esto resuelve, y ninguna de las tres es cosmética
① El rojo de O·1 se DISUELVE, no se parchea
o1-inventario-operaciones.ts dejó
un solo rojo estructural: sólo el owner alcanza una conexión. Medido: ningún paquete
de la plantilla (writer, reader, dml-writer, catalog-browser) menciona un
privilegio CONNECTION_*; el único rol que llega es operator —y llega de rebote,
por la implicación de WORKSPACE_MANAGE_CONTENT—. Consecuencia real: en
node-development un admin creó dos conexiones y no puede usar las suyas.
⛔ Repartir CONNECTION_USE por rol de membresía era la pregunta equivocada. Con
FOREIGN DATABASE, leer de fuera lo gobierna el mismo eje que leer de dentro
(TABLE_READ_DATA sobre la tabla foránea), y CONNECTION_USE se queda con lo que de
verdad significa —lo mismo que en Databricks: ver los detalles de la conexión y usarla
para ingerir—.
② Se acaba la DESAMBIGUACIÓN POR EXISTENCIA
Ha aparecido tres veces (C·4, C·5, F·1), y el 08-17 quedó escrito que ya no era
casualidad: «en una puerta que habla el mismo dialecto que el motor, nunca puede ser
sintáctica». Hoy, para saber si ventas.public.pedidos es del origen o del Warehouse,
hay que ir a la base a preguntar si existe una conexión que se llame ventas.
⭐ Con una base foránea eso desaparece: el primer segmento es un objeto declarado en nuestro catálogo, así que se resuelve como cualquier otro nombre. La rama especial que hoy vive en tres ejecutores se convierte en resolución normal.
③ El espejo hace navegable lo que hoy hay que ir a preguntar
SHOW TABLES IN <conexión>.<esq> va al origen cada vez. Con el espejo, el árbol del
Catalog Explorer, el linaje, las etiquetas y las políticas tienen algo a lo que
agarrarse — que es lo que convierte un origen en parte del catálogo y no en un tubo con
nombre.
3 · ⛔⛔⛔ EL INVARIANTE QUE NO SE NEGOCIA
El 08-16 se midió y se escribió que un FOREIGN CATALOG real es inviable: un catálogo
que el motor ve directamente deja la federación fuera del punto de aplicación — no se
debilitaría, desaparecería.
Esta entrega NO lo revierte, y hay que entender por qué no lo contradice.
Lo que Databricks llama foreign catalog no es un catálogo que el motor monte: es metadata espejada DENTRO de Unity Catalog, es decir, dentro del punto de aplicación. El motor no gana ningún camino: sigue pasando por UC.
⇒ Aquí, exactamente igual:
el usuario ──▶ LA PUERTA (runQuery) ──▶ resuelve `ventas.public.pedidos`
│ contra el ESPEJO (Index)
│ y autoriza con TABLE_READ_DATA
▼
lib/governance/jdbc-vista.ts ← LA PRIMITIVA, sin cambios
vista TEMPORAL en la sesión del motor
▼
el PostgreSQL del cliente
El espejo es metadata que vive en Index. Los DATOS se siguen resolviendo por la vista JDBC temporal. El motor no ve ninguna base foránea, ni antes ni después.
⚠️ Y esto se protege con un trinquete, no con un comentario: FD·6.
4 · ⭐⭐ La decisión de diseño que hay que tomar primero
Hoy una connection ya lleva host, port Y database. O sea: nuestra conexión
ya es de facto un servidor + UNA base, con las dos cosas fundidas en una fila. Eso
obliga a elegir, y la elección cambia toda la entrega:
| opción | qué implica | |
|---|---|---|
| A | La conexión ES el SERVER; la base foránea es un objeto NUEVO | CREATE FOREIGN DATABASE ventas USING CONNECTION c OPTIONS (database 'ventas'). Separa lo que el estándar separa. Un servidor puede tener N bases sin repetir credencial. ⇒ hay que migrar las 6 conexiones actuales, cuyo database pasa a ser una base foránea. |
| B | La conexión ES la base foránea (se le añade el espejo encima) | Cero migración: cada conexión ya apunta a una base. Pero fija «un servidor = una base», y para dos bases del mismo Postgres hacen falta dos conexiones y dos credenciales. |
⭐ Y la medida que inclina la decisión: de las 6 conexiones vivas, 4 apuntan a la
MISMA base (…neon.tech:5432/neondb) — cuatro filas, cuatro credenciales, un solo
destino. Eso es exactamente el síntoma que produce la opción B, ya ocurriendo hoy.
⇒ Recomendación: A. Pero la migración es real y se decide antes de escribir código, no a mitad.
5 · Las fases, y qué mide cada gate
Cada fase cierra con un gate que mide el sistema, no una copia suya — la disciplina que O·1 acaba de aplicar.
🏁 FD·0 · La medida de partida — CERRADA (2026-08-17)
scripts/ingesta/fd0-medida-de-partida.ts
6 conexiones ⇒ 3 SERVIDORES · 3 BASES FORÁNEAS
3 filas redundantes que la opción A retira
| ⚠️ 4 conexiones a la MISMA base | …neon.tech:5432/neondb — 4 filas, 4 credenciales, un destino. El desperdicio que A elimina, ya ocurriendo |
| ⛔⛔ 1 COLISIÓN de nombre | en node-development, neondb existe en dos servidores distintos ⇒ el database remoto no identifica ⇒ el nombre lo tiene que dar el usuario. Esto decidió la gramática de FD·1 |
| ✅ el espejo pesaría | 2 esquemas · 48 tablas · 4,1 s ← la línea base contra la que se juzgará FD·2 |
| ⛔ 1 credencial inservible | la de localhost: 409, no trae usuario y contraseña utilizables |
| ⚠️ 1 destino indeterminado | el RDS: el motor no llega (y se dice indeterminado, no muerto) |
⭐⭐ Los dos hallazgos de método, que valen más que los números:
① CUATRO estados, no tres. El 08-17 dejó escrito que había que distinguir ok · no
· indeterminado. La primera corrida sacó uno que no es ninguno: la cara denegando con
403. No es el origen diciendo que no, ni transporte fallido — es el plano de permisos
aplicando, y es el dato más firme de todos. Meterlo en indeterminado habría escondido
la única respuesta que no admite duda.
② ⛔⛔ El proveedor de Spark APLANA los rechazos de la cara.
CarbonJdbcConnectionProvider los reporta todos como «la cara denegó la credencial»,
así que desde el motor un 403 (falta el grant), un 404 (no hay tal conexión) y un
409 (la credencial no sirve) son indistinguibles. Se leyó como «denegado por
permisos» y era un 409. ⇒ el gate pregunta ahora por la puerta antes que por el motor,
donde vendConnectionCredential devuelve status y motivo de verdad. Es la misma regla
que connection-credential.ts ya defiende —«el motivo lo da el plano de permisos, no el
driver»—, sólo que el motivo se perdía al cruzar al motor.
🏁 FD·1 · El OBJETO y su gramática — CERRADA (2026-08-17)
CREATE FOREIGN DATABASE … USING CONNECTION … OPTIONS (database '…') ·
DROP FOREIGN DATABASE [IF EXISTS] · SHOW FOREIGN DATABASES.
Módulo puro (foreign-database-sql.ts) + ejecutor, el reparto de C·2. Securable nuevo con
cadena [workspace, connection, foreign_database]. Migración
20261291_foreign_databases.sql, aplicada y reaplicable.
🔒 fd1-gate-base-foranea.ts VERDE · 21 tests en frío · tsc limpio · 865 en la suite.
⭐⭐⭐ La sonda que decide, y que antes era imposible de escribir: se concede sobre
una base y la otra del mismo servidor sigue denegada. Con la conexión como único
objeto el permiso era el servidor entero, todo o nada. Medido también por el camino del
producto: el DROP de la denegada se rechaza y el de la concedida pasa.
⭐ Y esta gramática NO es ambigua — no comparte forma con nada del Warehouse, así que
se reconoce por el TEXTO y puede return en seco. Confirmado lo que §2·② anticipaba: lo
caro no era nombrar orígenes, era la otra forma de nombrarlos.
⭐⭐ Los dos hallazgos de FD·1:
① Una FK redundante rompe lo que deriva el esquema. La tabla nació con FK simple en
la columna y FK compuesta (connection_id, workspace_id). Son dos caminos al mismo
destino: PostgREST vio dos relaciones y SHOW FOREIGN DATABASES murió con «more than one
relationship was found» al pedir el nombre de la conexión. ⇒ se deja sólo la
compuesta, que es la que además exige la tenencia.
② Un DROP … IF EXISTS + ADD deja de ser idempotente en cuanto algo depende. En
connections_id_workspace_key, la segunda pasada falló con «cannot drop constraint …
because other objects depend on it» — la FK compuesta ya apuntaba ahí. ⇒ se añade con un
DO $$ IF NOT EXISTS $$. Lo destapó reaplicar la migración, que es exactamente para lo
que se reaplica.
FD·2 · EL ESPEJO — REFRESH FOREIGN { DATABASE | SCHEMA | TABLE }
La sincronización de metadata. Por el motor, con la misma vista JDBC sobre
information_schema — que es lo que O·2 acaba de hacer para la introspección, y por el
mismo motivo: la puerta y el motor no tienen la misma red.
Gate: refresca, detecta altas/bajas/cambios de tipo, y es idempotente (dos
pasadas dejan el mismo censo — la trampa que C·5 pagó con IF NOT EXISTS).
FD·3 · La RESOLUCIÓN y la autorización por la tabla
SELECT … FROM ventas.public.pedidos resuelve contra el espejo y autoriza con
TABLE_READ_DATA. Aquí muere la desambiguación por existencia.
Gate (el que decide): ⭐ una tabla foránea CON grant se lee y otra SIN grant se
deniega — control negativo por TABLA, no por conexión. Es lo que hoy es imposible.
FD·4 · La COMPATIBILIDAD del camino de hoy
<conexión>.<esq>.<tabla> funciona en producción. O se mantiene como alias, o se retira
con un aviso que diga qué escribir en su lugar.
⚠️ Decisión abierta — ver §7·②.
FD·5 · Lo que el espejo desbloquea, y que hoy es ∅ en O·1
Con tablas foráneas en el catálogo, tres de las cinco ausencias del inventario dejan de
necesitar gramática propia: DESCRIBE ventas.public.pedidos es un DESCRIBE, la vista
persistente sobre el origen es un CREATE VIEW normal, y el linaje deja de tener un
agujero donde empieza el dato.
FD·6 · 🔒 EL TRINQUETE del invariante
npm run check:sin-catalogo-federado — que ninguna base foránea se registre como
catálogo del motor. La propiedad de §3 no se puede comprobar con una aserción sobre un
valor, y el día que alguien la rompa el fallo aparecería como «qué bien, ahora va más
rápido».
6 · Los privilegios, alineados con el estándar
| privilegio | se concede sobre | qué da | equivalente Databricks |
|---|---|---|---|
CONNECTION_USE | conexión | ver sus detalles y usarla para ingerir | USE CONNECTION |
CONNECTION_CREATE | workspace | declarar un origen | CREATE CONNECTION |
CONNECTION_MANAGE_ACCESS | conexión | repartir su acceso | (ownership) |
FOREIGN_DATABASE_CREATE 🆕 | la CONEXIÓN | montar el espejo read-only de una base | CREATE FOREIGN CATALOG |
SCHEMA_USE · TABLE_READ_DATA | esquema / tabla foránea | leer los datos | USE SCHEMA · SELECT |
⭐ La fila que importa es la última: leer de fuera se concede con el privilegio de siempre. Ningún rol nuevo, ningún eje paralelo, y el reparto por rol de membresía deja de ser una pregunta abierta.
⚠️ Y FOREIGN_DATABASE_CREATE va sobre la conexión, no sobre el workspace, igual que
en Databricks: quien deja montar un espejo de una base es el dueño de esa conexión, no
quien administra el inquilino.
7 · ⚠️ Lo que queda abierto, y hay que decidir antes de escribir código
① 🏁 ¿Dónde vive el espejo en el régimen N·4? — RESUELTA, y por un motivo mejor que el
que la abrió. Ver rubix-federation-el-espejo.md.
Tablas propias (foreign_schemas · foreign_tables · foreign_columns) colgando de
foreign_databases. Fuera de datasets y fuera de N·4.
El argumento de partida era que una tabla foránea no tiene identidad física y el trinquete
la juzgaría mal. Cierto, pero secundario. El de verdad es el invariante RUBIX·I1:
datasets es la autoridad de datos del Warehouse, así que meter ahí el espejo lo
convertiría en autoridad de datos por construcción — y el espejo sólo puede ser autoridad
de gobierno.
② La compatibilidad de <conexión>.<esq>.<tabla>. Está en producción y es lo que F·1
dejó verde. Mantenerla como alias es barato; mantenerla para siempre significa
conservar la desambiguación por existencia que §2·② dice que se elimina — es decir,
quedarse con las dos cosas y la complejidad de ambas.
③ 🏁 La caducidad del espejo — RESUELTA, y la mitad de la pregunta se DISOLVIÓ.
Ver rubix-federation-el-espejo.md §3.
Sin refresco inline y sin refresco en tiempo de consulta. Databricks y Dremio los
necesitan porque su espejo es autoridad de planificación; con RUBIX·I1 el nuestro no
lo es, así que la consulta no lo consulta. Y por eso «¿qué pasa si discrepan a mitad de
una consulta?» no tiene respuesta: no pueden discrepar, porque la consulta no lee del
espejo.
⭐ Lo que sí hace falta es el estado de reconciliación por objeto, entero, al estilo
Denodo: nuevo · retirado · cambiado-en-origen · divergente (difiere y el origen
NO cambió ⇒ lo cambió alguien aquí ⇒ no se pisa). El cuarto es el que casi nadie
implementa y el que evita borrar trabajo en silencio.
③·bis ⛔⛔ Y el guardarraíl de seguridad que trae la investigación: la coordenada remota de una tabla foránea es inmutable tras crearse. Un refresco que la vea cambiada la marca, no la actualiza. Es el escenario de los authorized paths de Databricks — donde un origen manipulable convierte el refresco en escalada de privilegios— acotado a nuestro caso, que es más benigno porque el perímetro es la credencial JDBC del cliente y no una credencial de storage nuestra.
④ El ∅ que esto NO resuelve. Escribir en el origen (INSERT/MERGE) sigue siendo
una decisión no tomada, y el espejo no la toma: un espejo read-only es explícitamente
read-only en los tres sistemas de referencia.
⑤ Los tipos que el espejo no sabe traducir. El DESCRIBE sobre la vista JDBC devuelve
tipos ya en Spark SQL —por eso C·5 pudo tirar un mapa de tipos hecho a mano—. El
espejo hereda esa ventaja, pero también su límite: lo que el driver no sepa representar
llegará como algo que el catálogo tendrá que saber guardar.
8 · 📎 De dónde sale cada afirmación de este documento
SQL/MED · FOREIGN DATA WRAPPER, SERVER, USER MAPPING, IMPORT FOREIGN SCHEMA | ISO/IEC 9075-9:2008 · PostgreSQL, Foreign Data |
USE CONNECTION = «list and view connection details»; CREATE FOREIGN CATALOG sobre la conexión; leer = USE CATALOG+USE SCHEMA+SELECT | Unity Catalog privileges reference |
REFRESH FOREIGN { CATALOG | SCHEMA | TABLE } y el refresco en tiempo de consulta | REFRESH FOREIGN |
DATABASE.SCHEMA.OBJECT como namespace de tres niveles | Snowflake · Database, schema & share DDL |
| «sólo el owner alcanza una conexión» · 4 conexiones a la misma base · 16 operaciones vivas | scripts/ingesta/o1-inventario-operaciones.ts, corrido el 2026-08-17 |
| «la puerta y el motor no tienen la misma red» | timeout expired a los 11 s, mismo origen que federa — O·2, hoy |
| el veto al catálogo federado | f1-federacion-por-la-cara.md |