El corte limpio — por qué nuestras tablas son Postgres disfrazado, y por qué ya no hace falta migrar nada
Entregable (2026-07-30). Responde a dos cosas que el owner puso sobre la mesa y que invalidan el encuadre de los tres documentos anteriores de hoy:
- «¿Por qué nuestras tablas son tablas Postgres vestidas de Iceberg?»
- «Los datasets actuales son todos prescindibles — no entiendo la necesidad de migrarlos.»
La segunda es correcta, y está medida. Y convierte una migración de dos años en un corte.
1 · Por qué son Postgres disfrazado
Carbon fue un producto Postgres antes de ser un lakehouse. El dato vivía en dataset_rows:
dataset_rows (id, dataset_id, transaction_id, row_index, data jsonb, created_at)
Iceberg no llegó como sustituto: llegó como ESPEJO. La decisión de diseño —correcta entonces— fue que la migración tenía que ser invisible para los lectores: decenas de sitios ya consultaban dataset_rows, paginaban por row_index y resolvían filas por id. Reescribirlos todos antes de tener un solo byte en Iceberg habría sido inasumible.
Así que la tabla Iceberg se construyó para poder hacerse pasar por la tabla Postgres a la que sustituía:
Columna de dataset_rows | Su disfraz en Iceberg | Para qué |
|---|---|---|
row_index | __row_index | que la paginación keyset siguiera funcionando sin tocar un solo lector |
id | __row_id | que «editar esta fila» siguiera resolviendo |
created_at | __created_at | simetría (y hoy cero consumidores) |
El disfraz era la funcionalidad. Y de él sale, en cascada, todo lo demás: el SortOrder declarado sobre __row_index (para que el pruning imitara al índice de PG), el protocolo de reserva de rangos del fan-out (para que varios escritores no colisionaran en un contador que PG daba gratis), y el oráculo de frescura (para arbitrar entre los dos substratos mientras ambos tuvieran dato).
Nada de esto es deuda por descuido. Es el precio, correctamente pagado, de una migración que tenía que ser invisible.
El problema es que la factura sigue llegando cuando la razón ya no existe. Un disfraz sólo tiene sentido si hay alguien a quien engañar. Si los datos son prescindibles, no hay nadie.
2 · La premisa del owner, medida — y es más fuerte de lo que parece
Filas declaradas en datasets | 562.834 |
Filas reales en dataset_rows (todo el lado Postgres) | 6.039 |
| Las 6 tablas mayores (Olist, ~550k filas) | ya son NATIVAS — viven en Iceberg |
| Datasets nativos vacíos de verdad en PG | 13 de 15 |
La masa ya cruzó. Lo que queda en Postgres son 6.039 filas repartidas en datasets de prueba (test10, usuarios_pruebas, logs_sistema, Manually entered table, transform paths de ensayo).
La pasarela no está protegiendo una migración a medias: está custodiando una habitación casi vacía. Y el 90 % de la complejidad del sistema —oráculo, acta, flags, tres gates, identidad sintética, protocolo de rangos— existe para arbitrar sobre esas 6.039 filas.
Conclusión: no hay nada que migrar. La pregunta correcta no es «cómo movemos los datos» sino «cuándo borramos el camino viejo».
3 · Qué colapsa, exactamente
Todo lo que estos documentos planificaban como fases con canario era, en realidad, infraestructura para no perder datos. Sin datos que perder, desaparece:
| Lo planificado | Qué era | Con datos prescindibles |
|---|---|---|
W·1 · derivar row_count | reconciliar dos copias | innecesario: sólo hay una fuente |
| W·2 · retirar el oráculo con canario en 3 compuertas | no romper 45 pares expuestos | se borra. Los 45 pares dejan de existir |
| W·3 · partir el ledger | preservar la ventana incremental | se simplifica |
pasarela.md · «se queda sin población» | strangler fig, dos regímenes | no hace falta esperar: se corta |
Backfill de identidad, --identity | rescatar tablas pre-Fase-2 | se borran las tablas |
| El canario de paridad del polling | proteger sincronizaciones vivas | sigue valiendo, pero para datos nuevos |
fighter_jets (20 en PG vs 40 en catálogo) | incidente a investigar | se borra el dataset |
Y la decisión que bloqueaba todo —«¿se retira la paginación por __row_index?»— deja de ser una decisión de riesgo y pasa a ser una de diseño. No hay que preservar el comportamiento de nada: hay que elegir cómo se pagina un warehouse.
4 · Qué NO colapsa — y es la parte que hay que mirar
Esto es lo importante del documento, porque es lo que separa «una semana» de «un trimestre».
Datos prescindibles ≠ código prescindible. Borrar 6.039 filas no borra los sitios que saben leerlas.
-
178 referencias a
__row_indexen 43 ficheros. Los consumidores no se van con los datos: la cuadrícula, el export, el SDK NDJSON, el graph explorer, la edición de fila y la primitiva de paginación siguen escritos contra una columna que ya no existirá. Esto es el trabajo real, y no lo abarata la premisa. -
Kuzu usa
__row_indexcomoPRIMARY KEY. Es el consumidor más acoplado. Necesita una clave de verdad, y 25 de 106 datasets no tienen ninguna declarada. -
27.042 objetos de ontología. Esto no es dato de prueba en el mismo sentido: es la capa semántica, y sus filas apuntan a filas de dataset por
dataset_row_id. Si los datasets se borran, esos punteros quedan colgando. Es la única pieza donde «prescindible» hay que confirmarlo explícitamente, no asumirlo. -
La SDK v1 es una superficie publicada (
app/api/sdk/v1/…). Si algún consumidor externo la usa, su contrato de filas es un compromiso, no una decisión interna. (No lo sé; es pregunta para el owner.) -
Capacidades que hoy sólo tiene Postgres. El browse filtrado (
data::text ILIKE) está documentado como «una capacidad Postgres-only sin equivalente lakehouse». Lo es en el seam actual, pero DuckDB lo hace de sobra — sólo hay que escribirlo. -
Las branches (
dataset_branch_rows, overlay Lazy-COW) son PG puro y no tienen equivalente diseñado.
5 · El plan que sale de aquí: corte limpio, no strangler fig
La forma cambia por completo. No se construye un puente y se espera: se construye la casa nueva al lado, se apunta el producto a ella, y se demuele la vieja.
C·1 · Fijar la forma de la tabla limpia (diseño, sin datos)
Una tabla Iceberg sin columnas reservadas: sólo columnas de negocio. La identidad de fila es la clave primaria declarada (que ya vive en datasets.schema, en Index, que es donde el catálogo no llega) o ninguna, y entonces la tabla es de sólo lectura desde la cuadrícula — dicho en la UI.
Guarda: la matriz de 10 verbos de warehouse-writes-parity.md §1 como harness. Sobre una tabla limpia debe dar 10/10. Hoy da 4/10.
C·2 · El read path nuevo, sobre la tabla limpia
Paginación por resultado (LIMIT/OFFSET en DuckDB), filtrado por SQL real, direccionamiento de fila por PK. Aquí está el trabajo de verdad (§4·1), y no depende de ningún dato existente.
C·3 · Apuntar el producto
Los puertos del READER_REGISTRY dejan de resolver substrato: leen la tabla. El oráculo, el acta y los flags se borran — no se acotan.
C·4 · Demoler
dataset_rows, iceberg_sync_log, iceberg_freshness(), lakehouse_*_flags, iceberg_sync_cursor, el sync-poller, el protocolo de reserva de rangos, las tres columnas de identidad. Y los 106 datasets de prueba.
C·5 · Repoblar
Re-ingerir desde las fuentes lo que se quiera tener. Las fuentes conectadas siguen ahí; el Olist se vuelve a cargar en minutos.
Index no se toca en ninguna fase. Catálogo, esquemas, coordenada, tier, procedencia, permisos, ontología, la puerta y su validación de tenencia: todo eso es el pilar 3 y se queda en Postgres, que es donde debe estar. Lo que se demuele es el plano de datos de Postgres, no su plano de gobierno. Esa distinción es justo la que warehouse-index.md lleva un mes fijando, y es lo que hace que este corte sea seguro.
6 · Lo que hay que confirmar antes de cortar
- Los 27.042 objetos de ontología — ¿prescindibles también? Si no, hay que decidir qué los ancla cuando sus filas cambien de identidad.
- La SDK v1 — ¿hay consumidores externos con contrato?
- Las fuentes conectadas — repoblar exige que las credenciales sigan vivas. Hay 2 credenciales distintas en uso sobre 51 datasets
postgresql. - Las branches y los pipelines desplegados — un pipeline desplegado apunta a
output_dataset_id. Borrar datasets rompe pipelines; si también son prescindibles, no hay problema, pero hay que decirlo.
7 · Lo que esto le hace a los documentos de hoy
- warehouse-writes-parity.md — sigue siendo el correcto: el diagnóstico (
__row_indexbloquea la matriz de verbos) y la simplificación (paginar el resultado, no la tabla) no cambian. Lo que cambia es que F·1 deja de necesitar compatibilidad hacia atrás. - pasarela.md — su tesis (la pasarela es una pregunta mal puesta) sigue siendo verdad y explica por qué existe. Su plan (declarar substrato, dos regímenes conviviendo) sobra: era para no perder datos.
- warehouse-scaffolding-retirement.md — el plan W entero era una migración cuidadosa de datos que resultan ser prescindibles. Queda como inventario, no como plan.
La lección, y es la misma que se ha repetido todo el día en otra escala: siete premisas falsas costaron una iteración cada una. Ésta —«hay datos que preservar»— estaba debajo de todo el corpus, y nadie la había cuestionado.
Cruza con: warehouse-writes-parity.md (el objetivo y la matriz) · pasarela.md (por qué existe lo que se demuele) · warehouse-index.md (lo que NO se toca) · warehouse-scaffolding-retirement.md (el inventario).