Writes con paridad de Warehouse desde el SQL Editor — qué bloquea de verdad
Entregable (2026-07-30). El objetivo lo fijó el owner: escribir desde el SQL Editor como se escribe en Snowflake, Databricks, Neon o Supabase. Este documento reencuadra el trabajo alrededor de eso, con permiso explícito para simplificar la estructura.
Y el reencuadre cambia la prioridad. pasarela.md proponía retirar la pasarela como el trabajo siguiente. Con este objetivo, la pasarela es el bloqueante nº 2, no el nº 1. El nº 1 es más profundo, más barato, y consiste sobre todo en borrar código.
1 · El objetivo, convertido en algo comprobable
«Paridad» no es una sensación. Es esta matriz, sobre cualquier tabla del Warehouse:
| Verbo | Snowflake · Databricks · Neon | Carbon hoy |
|---|---|---|
SELECT | ✅ | ✅ |
CREATE VIEW · DROP VIEW | ✅ | ✅ |
ALTER TABLE … RENAME | ✅ | ✅ |
DELETE | ✅ | ⚠️ en 1 dataset de 106 |
MERGE | ✅ | ⚠️ en 1 dataset de 106 |
INSERT | ✅ | ❌ rechazado por la puerta |
UPDATE | ✅ | ❌ rechazado por la puerta |
CREATE TABLE · CTAS | ✅ | ❌ rechazado |
TRUNCATE · DROP TABLE | ✅ | ❌ |
ALTER TABLE … ADD COLUMN | ✅ | ❌ |
Estado real medido: el canary sql-editor.dml está flipeado para un dataset (animal_events). El resto no puede escribir nada. Y de los 106 datasets, sólo 15 son nativos: los otros 91 viven en Postgres, donde una escritura del editor la borraría la siguiente sincronización.
La distancia no es «faltan features». Es que dos tercios de la matriz están cerrados a propósito, por un motivo que el propio código nombra.
2 · La cadena causal, y la nombra nuestro propio código
Cuando la puerta rechaza un INSERT, el mensaje que devuelve es literalmente éste (junction.ts):
«Sólo DELETE y MERGE, que son los verbos que preservan la identidad de fila (
__row_index/__row_id), de la que dependen la paginación y el pruning.»
Tira del hilo y sale entera:
no se puede INSERT desde el SQL Editor
↑ porque la puerta lo rechaza (D·2)
↑ porque un INSERT rompe la monotonía de __row_index
↑ y además la extensión Iceberg de DuckDB rehúsa escribir en tablas ORDENADAS…
↑ …y TODA tabla nativa declara SortOrder ASC sobre __row_index
↑ porque el read path pagina por KEYSET sobre __row_index
↑ porque así paginaba `dataset_rows`, la tabla POSTGRES a la que sustituye
El SQL Editor no puede escribir como Snowflake porque nuestras tablas no son tablas de warehouse: son tablas de Postgres vestidas de Iceberg. Las tres columnas de identidad son el disfraz.
Y no es retórica — el servidor lo impone: read_service.py:133-138 lanza si se pide una lectura ordenada sobre una tabla sin __row_index. La paginación del producto entero se apoya en una columna que ningún otro motor sabe rellenar.
El coste que esto ya nos está cobrando
__row_indexno es una columna: es un protocolo distribuido. El fan-out reserva rangos disjuntos de2**40por partición con una columna de controlrow_index_base. UnINSERTde DuckDB no tiene forma de participar en ese protocolo. Añadir un motor no cuesta un driver: cuesta implementar el protocolo.- 178 referencias en 43 ficheros, incluidos Kuzu (donde se convierte en
PRIMARY KEY), el graph explorer, el export, el SDK, la edición de fila y la primitiva de paginación. - La protección que hoy nos tapa es accidental y ya tiene interruptor: DuckDB 1.5.4 trae
unsafe_iceberg_ignore_sort_order. Es decir, elINSERTes alcanzable hoy — sólo que rompería en silencio la promesa del SortOrder.
⭐ Y el argumento que lo cierra: la identidad sintética no cumple lo que promete
Se conserva porque «da un handle estable a cada fila». Medido, no lo da:
| Caso | ¿__row_id estable? |
|---|---|
| dataset espejado | sí (es dataset_rows.id) |
nativo con write_mode='upsert' y clave primaria | sí (uuid5 determinista) |
nativo replace/append sin clave | ❌ se re-acuña un uuid4 por fila en CADA snapshot |
Y __row_index se re-estampa desde 0 en cada replace. O sea: para las tablas sin clave —las únicas que necesitarían un identificador sintético— la identidad cambia entera en cada sincronización. Estamos pagando un protocolo distribuido, un SortOrder mentiroso y dos tercios de la matriz de verbos a cambio de una garantía que no existe donde haría falta.
3 · La simplificación: la paginación es del RESULTADO, no de la TABLA
Así es como funcionan los sistemas con los que queremos paridad. En Snowflake o Databricks nadie pagina la tabla: se pagina el resultado de una consulta. No hay una columna reservada dentro de los datos para que la cuadrícula pueda hacer scroll.
__row_indexexiste porque emulamos paginación de Postgres sobre un object store. La pregunta correcta nunca fue «¿cómo mantenemos__row_indexen todos los motores?», sino «¿por qué paginamos por índice?».
Y hay un dato que hace la respuesta mucho más barata de lo que parecía: ya tenemos la clave real. datasets.schema transporta isPrimaryKey, lo detecta ensurePrimaryKey en cada nacimiento, y está medido:
- 67 de 106 datasets con clave primaria declarada
- 25 sin ella · 14 sin esquema
La clave primaria es exactamente lo que el catálogo no puede saber y por eso vive en Index — el pilar 3 ya la posee. __row_index es una clave sintética que inventamos porque no nos fiábamos de la real.
Qué sustituye a qué
| Hoy | Mañana | Nota |
|---|---|---|
paginación keyset por __row_index | LIMIT/OFFSET sobre la consulta, ordenada por lo que el usuario pida | es lo que hace la vista previa de Databricks |
pruning por min/max de __row_index | estadísticas de fichero de Iceberg | es trabajo del formato, no nuestro |
| editar/borrar una fila desde la cuadrícula | por clave primaria | 67/106 ya pueden; las demás no podían de verdad tampoco (§2) |
__row_id para la ontología | row lineage de Iceberg v3, o la PK | ya está en el plan como W·4 |
__created_at | — | cero consumidores medidos. Se borra y ya |
4 · Lo que cae, y lo que hay que construir
Cae (es sustracción, no proyecto): el SortOrder declarado · el protocolo de reserva de rangos del fan-out · el gate icebergIdentityReady · la restricción de verbos de D·2 · la refusa de DuckDB (deja de aplicar: la tabla ya no está ordenada, sin flags unsafe_).
Hay que construir, y es poco:
- Paginación por resultado en la primitiva de browse (una consulta con
LIMIT/OFFSET, que DuckDB ya sirve). - Direccionamiento de fila por PK en los tres sitios que editan/borran una fila.
- Un camino honesto para las tablas sin clave: o se declara una, o la cuadrícula es de sólo lectura. Decirlo es mejor que un handle que cambia solo.
CREATE TABLEpor la puerta — hoy se rechaza porque nacería fuera del chokepoint. Ese chokepoint ya existe y ya valida tenencia (I·2), así que es cablear el DDL acreateDatasetArtifact, no inventar nada.
5 · El orden, y dónde encaja la pasarela
F·1 · Retirar la identidad de fila (sustracción · desbloquea la matriz de verbos)
Empezando por lo gratis: __created_at (cero consumidores). Después la paginación, que es la que sostiene a __row_index. Al final, quitar el SortOrder del chokepoint de creación — y con él caen solas la refusa de DuckDB y la restricción de verbos.
Guarda: la matriz de §1 como harness ejecutable — un fichero que intenta los diez verbos sobre una tabla de pruebas y reporta cuáles pasan. Hoy daría 4/10. Es el marcador del entregable.
F·2 · Declarar el substrato (= pasarela.md) Con los verbos ya legales, esto es lo que los hace legales en más de 15 datasets. Y sigue siendo cierto todo lo de aquel documento — sólo cambia de puesto.
F·3 · El DDL por la puerta
CREATE TABLE, CTAS, DROP, ALTER … ADD COLUMN. Es la última columna de la matriz y la más fácil una vez existe la puerta.
Por qué F·1 antes que F·2, y no al revés: declarar el substrato con la identidad de fila intacta te da
DELETE/MERGEen 15 datasets. Retirar la identidad con la pasarela intacta te da la matriz entera en esos mismos 15 — que es la forma del producto que se busca, en pequeño y comprobable. Es la fase que enseña si la tesis es cierta, y es la que sólo resta código.
6 · Costes y riesgos, sin adornos
OFFSETprofundo es O(n). Keyset es más rápido en la página 10.000. Es la misma propiedad que tienen Snowflake y BigQuery, y para una cuadrícula de UI no se nota; para el export no aplica (se transmite entero, sin paginar). Pero es una regresión real de rendimiento en el caso profundo y hay que decirlo antes.- 25 datasets sin clave primaria se quedan sin edición de fila. Hoy parecen tenerla. Esto convierte una promesa rota en una limitación declarada — mejor, pero visible para el usuario.
- Kuzu usa
__row_indexcomoPRIMARY KEY. Es el consumidor más acoplado y necesita su propio análisis; puede que sea el que decida el calendario. - Iceberg v3 sigue sin gate pasado (W·0). El row lineage nativo depende de él; mientras tanto, la PK es el sustituto, y para las tablas sin clave no hay sustituto.
- Nada de esto está medido en latencia. Ni la paginación nueva ni la vieja.
Criterio de parada, dicho por adelantado: si al retirar la paginación por keyset la cuadrícula se vuelve inutilizable en un dataset grande de producción, la tesis de §3 se cae y hay que volver a keyset — pero sobre la clave primaria, no sobre una columna sintética. Sería un repliegue, no una derrota: la conclusión seguiría siendo que la identidad sintética sobra.
7 · Decisiones para el owner
- ¿Se retira la paginación por
__row_index? Es la decisión; todo lo demás cuelga de ella. Recomendación: sí, y aceptar explícitamente elOFFSETprofundo. Es el precio de que cualquier motor pueda escribir. - ¿Qué pasa con las tablas sin clave primaria? Recomendación: cuadrícula de sólo lectura y decirlo en la UI, en vez de un handle que cambia en cada sync. Alternativa: obligar a declarar clave al crear.
- ¿
CREATE TABLEdesde el SQL Editor crea un objeto gobernado de pleno derecho? Recomendación: sí, por la puerta — con su coordenada, su tenencia validada y su carrier. Es lo que ya haceCREATE VIEWdesde hoy. - ¿Se acepta que la paridad llegue primero a los 15 nativos y luego al resto? Recomendación: sí. Es lo que hace la fase comprobable en pequeño antes de tocar los 91 espejados.
Cruza con: pasarela.md (F·2) · warehouse-scaffolding-retirement.md §3.3 y §6·3 (donde esta pregunta llevaba aplazada) · duckdb-conformance.md (la matriz de verbos medida) · warehouse-index.md (la PK vive en Index) · index-i2b-approach.md (la puerta que hace posible el DDL).