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
poseeCREATE 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 datasets | 0 de 5 — sólo view_definition, la cadena desnuda |
dialect · default_catalog · default_namespace · schema_id de la vista · version_id | ninguno existe |
| Vistas en TODO el sistema | 1 · 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 vista | NULL |
CREATE OR REPLACE | hace UPDATE: machaca, sin versión ni rastro |
| Percha para versionar | dataset_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
- 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 quedefault-namespaceexiste para impedir. - No hay versión inmutable. Un
CREATE OR REPLACEpisa 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 — dosbronze_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):
| Nivel | Requeridos | Opcionales |
|---|---|---|
| Metadata de la vista | view-uuid · format-version · location · schemas · current-version-id · versions · version-log | properties |
| Versión | version-id · schema-id · timestamp-ms · summary · representations · default-namespace | default-catalog |
| Representación SQL | type · 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
summarypuede llevarengine-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:
| Trino | SECURITY 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) |
| PostgreSQL | DEFINER por defecto; security_invoker = true desde la 15 para lo contrario |
| Carbon hoy | INVOKER — por accidente. La vista se expande a CTE y se resuelve contra la rebanada del lector. Nadie lo decidió |
Una vista
DEFINERes una herramienta de gobernanza; unaINVOKERes 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:
| Campo | Origen | Por qué |
|---|---|---|
sql | el cuerpo, ya cualificado | ver 2.2 |
dialect | requerido — "duckdb" | sin esto, cambiar de motor invalida el corpus en silencio |
default_catalog | opcional | resuelve las referencias sin catálogo |
default_namespace | requerido, LISTA de niveles (["main","default"]) | nuestro namespace es multi-nivel de nacimiento |
schema_id | el esquema con el que se creó | detectar deriva |
version_id | entero inmutable | un CREATE OR REPLACE crea versión, no machaca |
summary | engine-name/engine-version + quién | es 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: pidedefault-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 cuerpo | atar las referencias | |
|---|---|---|
| Qué guarda | texto cualificado | identidad, que es lo que no cambia si alguien renombra |
| Ambigüedad entre homónimos | la resuelve el texto… si el texto acierta | la resuelve una regla explícita, y si no puede, marca |
| Cuerpo que ve el usuario | distinto del que escribió | el suyo, intacto |
| Coste | un salto al motor por cada CREATE VIEW | ninguno: el catálogo ya está leído |
| Riesgo de reescritura de literales | el 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
| Entregable | Gate | Borra | |
|---|---|---|---|
| G1 · el contrato en Index | migración con los campos del spec + version_id inmutable | cero vistas sin dialect · un CREATE OR REPLACE crea versión y la anterior sigue consultable | view_definition como cadena desnuda |
| G2 · atar al crear | handleCreateView ata las referencias y persiste el contrato | una vista con homónimos ata al de su namespace, nunca al más reciente; el plan de lectura resuelve por la atadura | la resolución en contexto del lector |
| G3 · backfill | la única vista, atada y versionada | 1/1 · si no resuelve o es ambigua, marcada | — |
| G4 · decidir DEFINER/INVOKER | lo elegido, escrito y aplicado | un test que fije la semántica elegida | la ambigüedad actual |
4 · Riesgos
| Riesgo | Mitigación |
|---|---|
| Cualificar exige llamar al motor al crear una vista | crear 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 fin | version.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 principal | hoy la rebanada se construye por userId: es un parámetro, no una reescritura |
5 · Decisiones del owner
- ¿Columnas o un JSONB
view_contract? Recomiendo columnas paradialectyversion_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). - ⭐ ¿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.
- ¿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.
- ¿Vistas de verdad en Lakekeeper, ya? Recomiendo no todavía: una vista nuestra
lleva
tier, procedencia y linaje —cosas de Index—, y sacarla deldatasetsla borraría del árbol, deSHOW VIEWSy 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_customers
—main.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-idde 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
- Iceberg View Spec ·
format/view-spec.md— campos exactos, inmutabilidad,version.history.num-entries - CREATE VIEW · Trino · trinodb/trino#30 — current user security mode for views
- Authorized user and session user · Databricks
- Lakekeeper · API del catálogo — implementa la spec REST completa, vistas incluidas
- Medición propia (2026-08-01) — censo de vistas y de columnas contra la BD real de Index.