Published

El corte limpio — por qué nuestras tablas son Postgres disfrazado, y por qué ya no hace falta migrar nada

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

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:

  1. «¿Por qué nuestras tablas son tablas Postgres vestidas de Iceberg?»
  2. «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_rowsSu disfraz en IcebergPara qué
row_index__row_indexque la paginación keyset siguiera funcionando sin tocar un solo lector
id__row_idque «editar esta fila» siguiera resolviendo
created_at__created_atsimetrí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 datasets562.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 PG13 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 planificadoQué eraCon datos prescindibles
W·1 · derivar row_countreconciliar dos copiasinnecesario: sólo hay una fuente
W·2 · retirar el oráculo con canario en 3 compuertasno romper 45 pares expuestosse borra. Los 45 pares dejan de existir
W·3 · partir el ledgerpreservar la ventana incrementalse simplifica
pasarela.md · «se queda sin población»strangler fig, dos regímenesno hace falta esperar: se corta
Backfill de identidad, --identityrescatar tablas pre-Fase-2se borran las tablas
El canario de paridad del pollingproteger sincronizaciones vivassigue valiendo, pero para datos nuevos
fighter_jets (20 en PG vs 40 en catálogo)incidente a investigarse 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.

  1. 178 referencias a __row_index en 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.

  2. Kuzu usa __row_index como PRIMARY KEY. Es el consumidor más acoplado. Necesita una clave de verdad, y 25 de 106 datasets no tienen ninguna declarada.

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

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

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

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

  1. Los 27.042 objetos de ontología — ¿prescindibles también? Si no, hay que decidir qué los ancla cuando sus filas cambien de identidad.
  2. La SDK v1 — ¿hay consumidores externos con contrato?
  3. Las fuentes conectadas — repoblar exige que las credenciales sigan vivas. Hay 2 credenciales distintas en uso sobre 51 datasets postgresql.
  4. 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_index bloquea 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).