Published

FD · LA BASE FORÁNEA — approach de la entrega

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

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)DatabricksSnowflakeCarbon, hoy
FOREIGN DATA WRAPPER — el adaptador(implícito en el tipo)🏁 TYPE POSTGRESQL de CREATE CONNECTION (C·2)
SERVER — dónde vive el origenCONNECTION🏁 connections (C·2), securable con cadena [workspace, connection]
USER MAPPING — con qué credencialcredencial de la conexión🏁 vending por PERSONA con TTL (C·3) + CarbonJdbcConnectionProvider
FOREIGN TABLE — la tabla, consultabletabla del foreign catalogtablano existe
IMPORT FOREIGN SCHEMA — traerse las definicionesREFRESH FOREIGN …no existe
(el contenedor de arriba)FOREIGN CATALOGDATABASEno 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ónqué implica
ALa conexión ES el SERVER; la base foránea es un objeto NUEVOCREATE 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.
BLa 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 nombreen 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ía2 esquemas · 48 tablas · 4,1 s ← la línea base contra la que se juzgará FD·2
1 credencial inserviblela de localhost: 409, no trae usuario y contraseña utilizables
⚠️ 1 destino indeterminadoel 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

privilegiose concede sobrequé daequivalente Databricks
CONNECTION_USEconexiónver sus detalles y usarla para ingerirUSE CONNECTION
CONNECTION_CREATEworkspacedeclarar un origenCREATE CONNECTION
CONNECTION_MANAGE_ACCESSconexiónrepartir su acceso(ownership)
FOREIGN_DATABASE_CREATE 🆕la CONEXIÓNmontar el espejo read-only de una baseCREATE FOREIGN CATALOG
SCHEMA_USE · TABLE_READ_DATAesquema / tabla foránealeer los datosUSE 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 SCHEMAISO/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+SELECTUnity Catalog privileges reference
REFRESH FOREIGN { CATALOG | SCHEMA | TABLE } y el refresco en tiempo de consultaREFRESH FOREIGN
DATABASE.SCHEMA.OBJECT como namespace de tres nivelesSnowflake · Database, schema & share DDL
«sólo el owner alcanza una conexión» · 4 conexiones a la misma base · 16 operaciones vivasscripts/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 federadof1-federacion-por-la-cara.md