Published

M4 · Vistas con dialecto — 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...

M4 · Vistas con dialecto — approach

Entregable de planificación (2026-08-01). Convierte el milestone M4 de
carbon-sql-milestones.md en un plan ejecutable. Mismo
método que M3: medir primero, cotejar con quien ya lo resolvió, y declarar lo que
queda fuera
. Cruza con warehouse-sql-dialect-spec.md
§C1 · carbon-sql-m3-approach.md (la gramática que ya
posee CREATE VIEW) · warehouse-index.md.


0 · Lo medido — y hay una sorpresa de tamaño

Contra la BD real, el 2026-08-01:

Campos del Iceberg View Spec presentes en datasets0 de 5 — sólo view_definition, la cadena desnuda
dialect · default_catalog · default_namespace · schema_id de la vista · version_idninguno existe
Vistas en TODO el sistema1 · gold_order_risk_episodes · 1 dueño · 1 workspace · 2.150 chars · 2026-07-10
¿el cuerpo está cualificado?no: … from bronze_order_items group by order_id …
iceberg_namespace de esa vistaNULL
CREATE OR REPLACEhace UPDATE: machaca, sin versión ni rastro
Percha para versionardataset_schema_versions existe, 70 filas

El backfill es UNA fila. El plan trataba M4 como caro por el backfill; medido,
es el milestone más barato que queda. Y la ventana se cierra sola: cada vista que
nazca a partir de hoy encarece el movimiento.

Los dos peligros están vivos

  1. El cuerpo se resuelve en el contexto de QUIEN CONSULTA, no de quien creó la vista. Se persiste con nombres pelados, se expande a CTE y lo resuelve el AST del motor contra la rebanada del lector. Hoy no explota porque sólo hay un dueño; el día que dos principales tengan cada uno un bronze_order_items, la misma vista significa dos cosas. Es exactamente lo que default-namespace existe para impedir.
  2. No hay versión inmutable. Un CREATE OR REPLACE pisa el cuerpo: ni auditoría, ni vuelta atrás, ni forma de saber qué significaba la vista ayer.

✅ G1 · G2 · G3 · G4 — CERRADOS (2026-08-01) · leer §6

El contrato vive en Index con la forma del spec, las versiones son inmutables por
TRIGGER
, la vista está atada (DEFINER) y el camino de lectura resuelve por sus
ataduras. Y el backfill destapó lo que el censo insinuaba: la única vista era
ambigua
— dos bronze_customers, y ganaba el que se hubiera tocado más tarde.


1 · El estado del arte

1.1 · Iceberg View Spec — los campos EXACTOS

Leídos del spec, no de memoria (format/view-spec.md):

NivelRequeridosOpcionales
Metadata de la vistaview-uuid · format-version · location · schemas · current-version-id · versions · version-logproperties
Versiónversion-id · schema-id · timestamp-ms · summary · representations · default-namespacedefault-catalog
Representación SQLtype · sql · dialect

Y las tres reglas que importan:

  • «View versions are immutable. Once a version is created, it cannot be changed.»
  • La retención se gobierna con la propiedad version.history.num-entries — el historial no es infinito, es una decisión declarada.
  • El summary puede llevar engine-name / engine-version: quién escribió esto.

Varias representaciones por versión, una por dialecto — es la razón de que dialect sea requerido: el spec asume varios motores sobre la misma vista. Nosotros compramos el motor y ya tenemos dos en el horizonte (DuckDB hoy, Karma detrás de la puerta), así que el campo no es ceremonia.

1.2 · Lo que el spec NO decide, y los motores SÍ: ¿quién ejecuta la vista?

Éste es el hallazgo que más nos toca, y no estaba en el plan:

TrinoSECURITY DEFINER | INVOKER en CREATE VIEW. El defecto es DEFINER: las tablas de dentro se leen con los permisos del dueño de la vista, no de quien la consulta
Databricks / UC«Views … set the authorized user to their owner for all statements in the body»DEFINER, y es lo que hace que una vista sirva para GOBERNAR (enseñar un subconjunto sin dar acceso a la tabla)
PostgreSQLDEFINER por defecto; security_invoker = true desde la 15 para lo contrario
Carbon hoyINVOKER — por accidente. La vista se expande a CTE y se resuelve contra la rebanada del lector. Nadie lo decidió

Una vista DEFINER es una herramienta de gobernanza; una INVOKER es azúcar
sintáctica. Tenemos la segunda sin haberlo elegido, y con la tenencia por dueño de M3
ni siquiera se nota. Al llegar M5 —filtrado por privilegios— la diferencia deja de ser
teórica en el mismo commit.

1.3 · El destino ya existe: Lakekeeper implementa vistas

El plan decía «cuando las vistas migren a objetos-vista de Iceberg de verdad», en futuro. Medido: Lakekeeper implementa la spec REST completa, incluidos los endpoints de vistas, y además les da soft-delete con undrop. O sea que la migración no espera a que exista nada: es una decisión nuestra, y la de §5 la separa en dos pasos.


2 · El diseño

2.1 · Adoptar la FORMA del spec, en Index — no inventar un esquema

Cinco columnas nuevas (o un JSONB view_contract, §5), con los nombres del spec para que la migración futura sea un movimiento de sitio y no una traducción:

CampoOrigenPor qué
sqlel cuerpo, ya cualificadover 2.2
dialectrequerido"duckdb"sin esto, cambiar de motor invalida el corpus en silencio
default_catalogopcionalresuelve las referencias sin catálogo
default_namespacerequerido, LISTA de niveles (["main","default"])nuestro namespace es multi-nivel de nacimiento
schema_idel esquema con el que se creódetectar deriva
version_identero inmutableun CREATE OR REPLACE crea versión, no machaca
summaryengine-name/engine-version + quiénes lo que el spec pone ahí

2.2 · ATAR las referencias al crear — mejor que reescribir el cuerpo

⚠️ Corrección al propio approach, hecha al implementarlo. Esta sección decía
«cualificar el cuerpo al guardar, con el AST». Al ir a hacerlo salió que el spec no
pide eso
: pide default-namespace, y reescribir el texto —aunque sea por AST— es
más frágil y menos expresivo que la alternativa. Se cambió el mecanismo, no el
objetivo.

Lo que se persiste no es un cuerpo reescrito: son las ataduras (view_bindings: nombre lógico → dataset_id), fijadas en el momento de crear la vista.

reescribir el cuerpoatar las referencias
Qué guardatexto cualificadoidentidad, que es lo que no cambia si alguien renombra
Ambigüedad entre homónimosla resuelve el texto… si el texto aciertala resuelve una regla explícita, y si no puede, marca
Cuerpo que ve el usuariodistinto del que escribióel suyo, intacto
Costeun salto al motor por cada CREATE VIEWninguno: el catálogo ya está leído
Riesgo de reescritura de literalesel que M2c matóno existe: no se reescribe nada

Y es lo que hacen los motores: SECURITY DEFINER no reescribe la vista — la ejecuta en el contexto de su dueño. Atar es exactamente eso, resuelto en el momento correcto.

2.3 · El backfill: marcar, no adivinar

Una fila. Se ata con el default_namespace que tuviera al crearse (derivable de su coordenada) y lo que no resuelva —o sea ambiguo— se MARCA. Una vista sin atar es un fallo visible; una vista atada a la tabla equivocada es un fallo que miente.


3 · Fases

EntregableGateBorra
G1 · el contrato en Indexmigración con los campos del spec + version_id inmutablecero vistas sin dialect · un CREATE OR REPLACE crea versión y la anterior sigue consultableview_definition como cadena desnuda
G2 · atar al crearhandleCreateView ata las referencias y persiste el contratouna vista con homónimos ata al de su namespace, nunca al más reciente; el plan de lectura resuelve por la atadurala resolución en contexto del lector
G3 · backfillla única vista, atada y versionada1/1 · si no resuelve o es ambigua, marcada
G4 · decidir DEFINER/INVOKERlo elegido, escrito y aplicadoun test que fije la semántica elegidala ambigüedad actual

4 · Riesgos

RiesgoMitigación
Cualificar exige llamar al motor al crear una vistacrear no es caliente; y si el motor no está, se rechaza la creación en vez de guardar una vista sin cualificar (fail-closed)
El histórico de versiones crece sin finversion.history.num-entries, como el spec — declarado en la superficie publicada
M4 se convierte en «un dialecto por acumulación»la gramática está congelada desde M3; esto añade metadata, no sintaxis
DEFINER exige saber ejecutar como otro principalhoy la rebanada se construye por userId: es un parámetro, no una reescritura

5 · Decisiones del owner

  1. ¿Columnas o un JSONB view_contract? Recomiendo columnas para dialect y version_id (se filtran y se indexan) y JSONB para el resto. Un JSONB entero repetiría el error del carrier de procedencia: «el bloque más importante vive en un JSONB de la otra mitad del producto» (warehouse-index.md §4).
  2. ⭐ ¿DEFINER o INVOKER? Recomiendo DEFINER, que es el defecto de Trino, de Databricks y de Postgres, y lo que convierte una vista en gobernanza. Es la decisión con más consecuencias del milestone y hay que tomarla antes de M5.
  3. ¿Alcance del contrato de dialecto: sólo vistas, o todo SQL persistido? (§7.3 de la spec, sigue abierta). Para vistas el backfill es 1 fila; para pasos de pipeline y expectativas, hay que censarlo antes de prometerlo.
  4. ¿Vistas de verdad en Lakekeeper, ya? Recomiendo no todavía: una vista nuestra lleva tier, procedencia y linaje —cosas de Index—, y sacarla del datasets la borraría del árbol, de SHOW VIEWS y de la gobernanza. Adoptar la forma ahora hace que ese movimiento sea, más adelante, un cambio de sitio.

6 · Lo entregado (2026-08-01)

G1 · el contrato, y dos invariantes que impone la BASE

Migración 20261263_view_contract.sql, con los nombres del spec: view_dialect · view_default_catalog · view_default_namespace (text[], lista de niveles) · view_bindings · view_version_id · view_summary, más la tabla dataset_view_versions.

Lo que no es una promesa, medido en vivo tras aplicarla:

⛔ UPDATE dataset_view_versions …  → «las versiones de una vista son INMUTABLES
                                      (Iceberg View Spec)»            ← trigger
⛔ INSERT de una vista sin dialecto → viola datasets_view_contract_ck  ← CHECK

La migración se auto-verifica (cuenta vistas vs contratos vs versiones y lanza si no cuadran): la lección de la demolición del plano PG — no basta con que el comando no falle, hay que mirar el resultado.

G2+G4 · DEFINER, y por qué dejó de ser teórico

El censo insinuaba el riesgo; el backfill lo confirmó: hay dos bronze_customersmain.test con 99.441 filas y main.default con 30— y la vista los pedía por nombre pelado. Hoy ganaba el que se hubiera actualizado más tarde: el día que alguien tocara la copia pequeña, la vista pasaba a leer 30 filas. No fallaba: mentía.

view-contract.ts ata con una regla de tres pasos y sin desempatar por fecha: ① lo que esté en el namespace de la vista · ② si no, el único que haya · ③ si hay varios y ninguno suyo, AMBIGUA y se marca. Una referencia cualificada manda sobre todo.

Y el camino de lectura lo honra: buildDuckReadPlan expande el cuerpo resolviendo por las ataduras, no por el nombre. Si la atadura apunta a algo que el lector no ve, se dice — re-atar en silencio sería el fallo que esto existe para impedir. (La elevación de privilegios que DEFINER implica —leer a través de la vista algo que no ves directamente— NO se activa: hoy no puede darse, y fingirla antes de M5 sería un agujero.)

G3 · el backfill: 1 fila, 7 referencias

── gold_order_risk_episodes  (main.test)  v1
   ✅ bronze_order_items    → main.test (112.650)
   ✅ bronze_customers      → main.test (99.441)   ⚠️ había 2 con ese nombre
   … 7 referencias atadas
   → ATADA · v2
historial: v1 (sin atar, G1) → v2 (atada, G3)

Va en seco por defecto. Y el historial cuenta la verdad: la v1 registra que la vista nació sin atar. Eso es historia, no basura.

Verificado de punta a punta

contrato: v2 · 7 referencias atadas
plan de lectura: 7 tablas base · vista expandida: true
bronze_customers → 98094e4a · ¿el de main.test (99.441)?  ✅ SÍ (atada)

Gates: vitest 332/332 (15 nuevos) · tsc 0 · matriz de catálogo 64/64 · la migración auto-verificada, verde contra la BD real.

Lo que queda fuera, dicho

  • La elevación de privilegios de DEFINER — va con M5, donde hay privilegios de verdad. Hoy el catálogo es por dueño y una query cross-workspace ya se rechaza.
  • schema-id de la vista (el spec lo pide en la versión): exige calcular el esquema de salida del cuerpo, que es preguntarle al motor. Va a M5, que es donde la metadata pasa a servirse desde Index.
  • Vistas de verdad en Lakekeeper: decidido todavía no (§5.4). Con la forma adoptada, ese día es un movimiento de sitio.

Fuentes