Published

Runbook · desplegar la demolición del plano de datos PG

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

Runbook · desplegar la demolición del plano de datos PG

Qué despliega esto. El commit 467d266 (merge 11bf08d): D·1..D·4 + el paso 0 de D·5 de
legacy-plane-removal.md. El código deja de tocar
dataset_rows. La tabla NO se suelta aquí — el DROP está vetado y va después, con su
propio pre-vuelo.

Quién debe leer esto antes de tocar nada. Quien vaya a hacer git push origin main, porque
ese push es el despliegue de Next, y no puede ir primero.


0 · La regla que gobierna todo: ml-runner ANTES que Next

No es una preferencia de orden: al revés falla en silencio, que es el peor modo de fallo posible.

Node NUEVO  →  ml-runner VIEJO
   manda `offset` / `order_by` / `existing_rows`
   Pydantic ignora los campos que no conoce, sin error y sin log
   ⇒ el ml-runner devuelve LA PRIMERA PÁGINA para TODAS las páginas

Nadie ve una excepción. Nadie ve una traza. El usuario ve una tabla que "funciona" y repite la primera página hasta el infinito. Esto es P·1 (la paginación pasó a ser una ventana del resultado: offset+limit sobre un order_by declarado) y P·3 (start_offsetexisting_rows), y los dos viajan en este mismo commit.

Al revés —ml-runner nuevo, Node viejo— no pasa nada: el ml-runner nuevo sigue sirviendo lo que el Node viejo le pide. Por eso el orden es éste y no hay ventana de riesgo.

OrdenQué pasa
✅ ml-runner → NextSin ventana. El ml-runner nuevo atiende a los dos Nodes
⛔ Next → ml-runnerPaginación rota en silencio en todo lo que lea por la puerta

1 · Antes de empezar

  • main está en 11bf08d (o posterior) y verde: tsc limpio · los 3 trinquetes · 1.463 tests en las suites tocadas.
  • Los 2 rojos de projection-sql.test.ts son PRE-EXISTENTES — medidos antes de esta línea de trabajo. No son un bloqueante; no los persigas.
  • ⚠️ Los tests de pyiceberg NO corren en local (Python 3.14, sin wheels). Corren de verdad en ml-runner-tests.yml (3.12). Confirma que ese workflow está verde antes de desplegar ml-runner — es la única verificación real que tiene el lado Python.

2 · Desplegar ml-runner (Railway) — PRIMERO

services/ml-runner/railway.json. Cambios que van: el contrato de lectura (offset/order_by/ snapshot_id), el writer sin identidad sintética, y el borrado del espejo (sync_service.py + la ruta POST /lakehouse/sync).

Verificar al terminar:

  • GET /lakehouse/health → 200
  • POST /lakehouse/sync404. Es correcto: el espejo se borró en D·1c (la ruta ya no está registrada en el router). Si responde 200, estás contra el ml-runner viejo — no sigas al §3.
  • Una lectura paginada (POST /lakehouse/read) devuelve páginas distintas para offset=0 y offset=N. Cierra el modo de fallo del §0 — hazla, aunque parezca trivial: es justo el fallo que no da error.
  • El e2e de la facade, obligatorio — no es opcional y no lo sustituye ninguna de las tres de arriba:
npx dotenv -e .env.local -- npx tsx scripts/dataspaces/facade-smoke.ts

⚠️ Por qué el /health verde NO basta: pasó de verdad

La primera vez que se desplegó esto, /lakehouse/health daba 200, /lakehouse/sync daba
404 (o sea, era el build nuevo), el servicio estaba perfectamente arrancado… y
POST /lakehouse/ingest devolvía 500. La escritura nativa estaba caída entera.

La causa: al borrar la ruta del espejo se fue con ella _aiter_ndjson, un helper compartido
que /lakehouse/ingest seguía llamando. Python resuelve los nombres en ejecución, así que el
módulo importa, el servicio arranca sano y el fallo sólo aparece cuando alguien pisa esa línea.
No hay compilación que lo cace y tsc no mira Python. Había tres casos así (_aiter_ndjson,
_row_id, offset/n en el writer).

Un health-check comprueba que el proceso vive. Sólo un e2e que ESCRIBA comprueba que el camino
de escritura existe.
Desde entonces hay un lint de undefined name en ml-runner-tests.yml
(auto-verificado con un caso de control), pero el smoke sigue siendo la red de seguridad: corre
contra la infra real, no contra mocks.

⭐ Lo que este despliegue ENTREGA, y conviene medir aquí

_create_native_table ya no declara SortOrder sobre __row_index. Ésa era la punta de la
cadena: DuckDB rehúsa escribir en tablas ORDENADAS → toda tabla nativa declaraba SortOrder →
INSERT y UPDATE eran imposibles → la puerta los rechazaba. El motivo por el que 6 de los 10
verbos estaban cerrados ha desaparecido.

Primera tarea recomendada tras este despliegue: re-medir la matriz de verbos con
scripts/duckdb/verb-conformance.ts sobre una tabla creada nueva (sin la declaración).
Marcador de referencia: 4/10. Ojo: hay que crear la tabla después del despliegue — las que ya
existen conservan su SortOrder.


3 · Desplegar Next (Vercel) — DESPUÉS

git push origin main. Este push ES el despliegue.

Lo que se rompe A PROPÓSITO — no es un incidente

Decisión explícita del owner: «viven en el plano PG que estamos suprimiendo; volverán con la spec Iceberg». Todos fallan en voz alta, con un código que nombra la causa — nunca un no-op silencioso. Si soporte recibe uno de éstos, no es una regresión que investigar:

SuperficieRespuesta
Builds de pipeline (write de output del runtime)lanza · lib/pipelines/runtime.ts
Ingesta transaccional de la SDK v1 (open/append/commit/abort)410
ingest.stream410 ingest_stream_closed
Editar / borrar una fila409 row_not_addressable
Expandir el grafo por la ruta PG410 graph_expand_closed
kuzu-service hydrator · apply-schema file-backed · snapshot de checkpointcerrados

Inventario completo en §7 de la spec.

Verificar al terminar

  • El grid de datasets pagina de verdad (página 2 ≠ página 1). Mismo modo de fallo del §0, ahora desde la UI.
  • El grid de objetos sirve propiedades no vacías. Es la comprobación de que 20261228 aterrizó bien: esos 5.193 objetos tenían properties = '{}' y toda su información vivía en el plano.
  • Un media set lista sus ficheros (el manifiesto, ahora en Index).

4 · Aplicar las 4 migraciones restantes — sólo con Next ya desplegado

20261228 y 20261258 ya están aplicadas (2026-07-31). Estas cuatro no podían aplicarse antes porque el código viejo las necesitaba vivas — verificado en main, no supuesto:

MigraciónPor qué esperaba
20261257read-router.ts y write-router.ts consultaban lakehouse_read_flags/write_flags vía makeFlagLoader en cada lectura y cada escritura
20261259dataset_branch_rows se usaba en 8 sitios, incluidos junction.ts (la puerta) y lib/branches/queries.ts
202612563 rutas vivas de sdk/v1/…/transactions llamaban esas RPC
20261255el fan-out del sync-worker usaba esas columnas
npx tsx scripts/apply-migration.ts supabase/migrations/20261255_drop_row_index_reservation.sql
npx tsx scripts/apply-migration.ts supabase/migrations/20261256_drop_legacy_plane_rpcs.sql
npx tsx scripts/apply-migration.ts supabase/migrations/20261259_branch_rpcs_drop_row_overlay.sql
npx tsx scripts/apply-migration.ts supabase/migrations/20261257_drop_seam_control_tables.sql

20261259 antes que 20261257, y 20261256 antes que 20261259: PL/pgSQL resuelve las
tablas en ejecución, así que soltar dataset_branch_rows mientras una RPC la nombra rompería
el PR de notebooks. Primero se vacían los cuerpos, luego cae la tabla.

Y una que no es una migración pero va aquí:

  • Re-correr el backfill de 20261228. Es idempotente (WHERE base_properties IS NULL), así que capturar cualquier objeto nacido entre aquella migración y este despliegue cuesta re-aplicar el fichero. Hazlo: es barato y cierra la única ventana que dejó.

5 · Re-correr el censo

npx tsx scripts/dataspaces/d5-preflight-census.ts

Sólo-lectura. Ocho comprobaciones en dos mitades — el dato (①–⑤) y el estado (⑥–⑧) — y las dos vetan. Tras el paso 4 deberían quedar exactamente estos ⚠ en ⑦, que son el trabajo siguiente y no los resuelve este despliegue:

⚠ ESCRIBE  sdk_commit_file_batch              ← muda su destino a dataset_file_manifest
⚠ ESCRIBE  reap_orphan_checkpoint_datasets    ← ciérrala
⚠ lee      fn_sync_objects_after_ingest       ← ciérrala
⚠ lee      sdk_healthz_strict                 ← ciérrala

…más los 11 punteros de manifiesto aún en el plano (③bis) y la FK objects.objects_dataset_row_id_fkey (⑤). Con eso resuelto, el censo da VÍA LIBRE y entonces se emite el DROP.

Que ① y ② den cero no es vía libre. El dato lleva intacto todo este tiempo (73/73 espejados,
cero filas que perder) y el DROP sigue sin poder darse. Es la lección de esta spec: un
trinquete a cero mide el REPO, no el SISTEMA.


6 · Si hay que revertir

  • Nextgit revert del merge 11bf08d y push. Vercel redespliega.
  • ml-runner — rollback al deploy anterior en Railway.
  • Las migraciones NO se revierten sueltas. 20261228 y 20261258 son aditivas y seguras con las dos versiones del código: déjalas. Las cuatro del §4 sí asumen el código nuevo — si reviertes Next, revierte antes esas cuatro, o el código viejo se encontrará sin las tablas de flags que consulta en cada operación.