Published

Errores gobernados — approach

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

Errores gobernados — approach

Fecha: 2026-08-12. Cómo llevar el manejo de errores del SQL Editor al nivel de
Databricks/Snowflake, sobre el sustrato que ya tenemos.

Regla de la casa: cada hecho lleva el comando que lo demuestra. Lo que no lo lleva
va marcado como pendiente u opinión.

Se ordena detrás de parser-al-motor-spec.md — el
paradigma de verbos ricos gobernados —, y el §7 explica por qué ese orden no es
arbitrario. Manda sustrato-06.md donde discrepen.


1 · La pregunta

«Los errores que salen en el output del editor, en español — ¿cómo los gestionamos?
Databricks y Snowflake tienen un manejo muy maduro. ¿Qué tecnologías usan? ¿Es
nuestro sustrato suficientemente maduro para algo de ese calibre?»

Sí, y la parte difícil ya está hecha. Lo que falta no es arquitectura: es transporte y registro. Ahora mismo estamos recibiendo errores estructurados del motor y aplanándolos a texto antes de que nadie pueda usarlos.


2 · Lo medido — nuestro estado, hoy

✅ Lo que ya está bien, y no es poco

Hay una taxonomía tipada, no cadenas sueltas (contract.ts:487):

QueryRejection = 'unresolved-table' | 'not-readable' | 'no-tables' | 'cross-workspace'
               | 'no-workspace' | 'write-not-enabled' | 'verb-not-allowed' | 'plan-failed'

Y QueryRejectedError lleva reason y detail: Record<string, unknown>, con el comentario «datos para que la superficie construya un mensaje útil». Eso ya es, conceptualmente, un mapa de parámetros de mensaje. La intención estaba bien desde el principio; lo que falta es que sobreviva al viaje.

⭐ Y not-readable«el principal lo VE (BROWSE) pero no puede LEERLO»— demuestra que la distinción fina ya se pensó. Volveremos a ella en §7, porque es justo donde OpenFGA deja de ser un lujo.

⛔ Los tres agujeros

QuéDónde
1El error del MOTOR se aplana a texto antes de salir del puente_resumir_error(e) -> str en services/spark-bridge/app.py:60
2Dos nombres para el mismo campo en la superficie HTTP: un camino manda rejection, otro rejectedapp/api/sql-editor/execute/route.ts
3Los errores de motor pierden el código entero: sólo viaja err?.messageídem

Medido en vivo durante el canary de P·5:

AnalysisException: [TABLE_OR_VIEW_NOT_FOUND] The table or view `p4_fx_a` cannot be found…

TABLE_OR_VIEW_NOT_FOUND es un identificador estable de Spark. Nos está llegando — dentro de una frase, como texto, para que alguien lo lea con los ojos.


3 · Lo que hacen los líderes

Apache Spark 4 tiene un framework de condiciones de error: un error-conditions.json central, cada condición con SQLSTATE ANSI de 5 caracteres, mensajes parametrizados y subcondiciones (INCOMPLETE_TYPE_DEFINITION.ARRAY). La API expone getCondition(), getSqlState() y getMessageParameters() desde 4.0.0.

Nosotros corremos spark 4.1.2-stackable26.7.0. No hay que construirlo: hay que dejar de tirarlo.

Databricks construye encima: reserva la clase SQLSTATE KD para las suyas y XX para errores internos que merecen bug report. Y deja escrita la regla de diseño:

Para manejar un error programáticamente, usa la condición, el SQLSTATE y los
parámetros
— no el texto. El texto puede cambiar o localizarse sin aviso.


4 · ⭐⭐⭐ La tesis: el texto no puede ser la IDENTIDAD

El problema no es que nuestros mensajes estén en español.

El problema es que el texto en español es hoy la ÚNICA identidad del error.

Cuando la identidad es condición + SQLSTATE + parámetros, el texto queda libre: puede estar en español, traducirse, o reescribirse mañana sin romper a nadie. Es al revés de como suena — la estructura es lo que PERMITE localizar.

Y desbloquea lo que hoy es imposible: la UI puede subrayar la tabla que no existe en el editor, porque sabe cuál es (relationName viene en los parámetros), en vez de tener que sacarla de una frase con una expresión regular.


5 · El plan — E·0 a E·5

Cada fase con su gate. Ninguna se declara hecha sin él.

QuéGate
E·0El puente deja de aplanar. En su except BaseException, devolver {condition, sqlstate, messageParameters, message} en vez de una cadena. Es nuestro código y son ~10 líneasuna sonda que provoque 5 errores distintos y compruebe que los 5 traen condición y SQLSTATE
E·1El contrato de transporte, UNO. Un solo campo en la respuesta HTTP —no rejection y rejected— con la forma {condition, sqlstate, params, message} para los dos orígenes: la puerta y el motorun test de la ruta por cada origen · y un trinquete que falle si aparece un segundo nombre
E·2El catálogo de condiciones de Carbon. Nuestros 8 motivos pasan a condiciones con nombre estable y SQLSTATE propio en la clase KD (siguiendo a Databricks), documentadas en un fichero que se GENERA, como el contrato de Carbon SQLcheck:error-conditions — el doc casa con el código
E·3Parámetros de verdad. detail deja de ser libre y pasa a estar declarado por condición: unresolved-table lleva {relationName}, cross-workspace lleva {workspaces}tipos por condición · el test falla si una condición se lanza sin sus parámetros
E·4La UI reacciona por condición, no por texto: subrayar el identificador que falló, ofrecer la acción que corresponde, y el texto pasa a ser una plantillae2e por condición en el editor
E·5Localización. Sólo cuando E·2-E·4 estén: traducir antes es traducir la identidadel mismo e2e en dos idiomas

⚠️ E·0 antes que todo, y no por facilidad: mientras el puente aplane, cualquier cosa que construyamos encima estará adivinando sobre una cadena. Es el mismo error que sql-parse.ts cometía con el SQL, y del que hemos tardado una semana en salir.


6 · Lo que este plan NO es

  • No es traducir mensajes. Es darles identidad para que traducirlos sea seguro.
  • No es un catálogo de mensajes bonitos. Nuestros mensajes ya explican por qué y qué hacer mejor que la media. Lo que no tienen es con qué reaccionar.
  • No es un envoltorio genérico de errores. Los errores de infraestructura (workspace_lookup_failed, el 503 del puente) son otra familia: ahí el trabajo es reintentar y propagar el motivo, no clasificar semántica de SQL.

7 · ⭐⭐ OpenFGA — qué papel tiene, y por qué los errores lo NECESITAN

7·1 · Dónde estamos, medido

La retícula ya existe y aplica: CATALOG_OPS (catalog-authz.ts:58) mapea operación → privilegio → alcance, y decidePrivilege resuelve por cadena, o sea con herencia. La tabla de grants tiene la forma de securables de Unity Catalog:

principal_grants ......... 368 filas
por tipo de principal .... service 368   ⛔ CERO de usuario
columnas ................. workspace_id · principal_kind · principal_id ·
                           principal_role · catalog_role · privilege ·
                           securable_kind · securable_id

Lo que falta no es un motor de políticas: son los grants de usuario. Eso es P·2, y es independiente de OpenFGA.

7·2 · Para qué es NECESARIO OpenFGA, entonces

Tres cosas que una tabla de grants no expresa bien, y que llegan sí o sí:

  1. Relaciones, no filas. «El equipo X hereda de la carpeta Y», «este usuario es dueño de este esquema, luego puede todo lo de dentro», «me compartieron esta tabla». Con grants planos, cada una de esas frases se convierte en N filas que hay que mantener sincronizadas. ReBAC las expresa como lo que son: relaciones.
  2. El grafo. El norte es que la ontología herede la gobernanza del catálogo por relación (catalogo-origen-del-grafo.md). Eso es literalmente un problema de ReBAC — y gobernar el traversing no se puede con una tabla de privilegios por objeto.
  3. Ya autoriza al catálogo. OpenFGA está desplegado y es quien autoriza a Lakekeeper. O sea: ya es el aplicador en el punto de acceso. Tenerlo también del lado del producto elimina dos modelos de autorización para el mismo warehouse.

7·3 · ⭐⭐⭐ Y el vínculo con los errores, que es el hallazgo de este documento

La forma de un error es una decisión de autorización.

Cuando un usuario pide una tabla que no puede ver, hay dos respuestas posibles y no son equivalentes:

«esa tabla no existe»       →  no filtra nada, pero MIENTE
«no tienes permiso»          →  es cierto, pero CONFIRMA QUE EXISTE

La segunda es una fuga: revela la existencia de un objeto y a menudo su nombre. Los catálogos maduros eligen a propósito y por caso, y para elegir hace falta saber qué le está permitido SABER a ese principal — que es una pregunta de autorización, no de mensajería.

Y nosotros ya tenemos media respuesta escrita: not-readable distingue «lo VE pero no puede LEERLO» de «no existe». Esa distinción sólo tiene sentido con un modelo de autorización que separe BROWSE de SELECT — exactamente lo que ReBAC modela y una tabla de grants plana aproxima.

E·2 (el catálogo de condiciones) no se puede cerrar bien sin decidir esto. Se puede empezar sin OpenFGA —las condiciones de sintaxis, dialecto y planificación no dependen de él— pero las condiciones de acceso sí, y son las que más se ven.

7·4 · El orden que se deduce

verbos ricos gobernados (P·4 · P·5)      ← EN CURSO. El primero ya está en producción
   └─ E·0 · E·1        el transporte del error          no depende de nada
        └─ P·2         los grants de USUARIO            desbloquea la retícula para humanos
             └─ E·2 · E·3   las condiciones de acceso   necesitan saber qué puede SABER cada uno
                  └─ OpenFGA    cuando las relaciones superen a las filas
                       └─ E·4 · E·5   la UI y la localización

OpenFGA no es el siguiente paso, y tampoco es opcional. Es el paso en el que relaciones sustituyen a filas — y ese momento lo marca el producto (compartición, equipos, el grafo), no una preferencia técnica.


8 · Los riesgos, sin maquillar

  1. Un catálogo de condiciones es un CONTRATO. En cuanto la UI reaccione a unresolved-table, renombrarlo rompe clientes. Por eso E·2 exige que se genere y se trinquete, como el contrato de Carbon SQL.
  2. Las condiciones del motor no son nuestras. TABLE_OR_VIEW_NOT_FOUND lo define Spark y puede cambiar con la versión. Hay que decidir si se reexportan tal cual (atándonos a su versionado, que es lo que hace Databricks) o se traducen a las nuestras en la frontera. La primera es más honesta y más barata; la segunda aísla.
  3. Los parámetros pueden filtrar. messageParameters lleva nombres de tablas y columnas. Lo que se devuelve al cliente pasa por la misma decisión de §7·3, y por defecto no se reenvían sin mirar.
  4. Es fácil confundir familias. Un 503 del puente y un TABLE_OR_VIEW_NOT_FOUND no se arreglan igual ni se cuentan igual. Mezclarlos en un envoltorio genérico daría un número alarmante y falso — §12·39.

9 · Fuentes