Runbook · desplegar la demolición del plano de datos PG
Qué despliega esto. El commit
467d266(merge11bf08d): 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í — elDROPestá 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_offset → existing_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.
| Orden | Qué pasa |
|---|---|
| ✅ ml-runner → Next | Sin ventana. El ml-runner nuevo atiende a los dos Nodes |
| ⛔ Next → ml-runner | Paginación rota en silencio en todo lo que lea por la puerta |
1 · Antes de empezar
-
mainestá en11bf08d(o posterior) y verde:tsclimpio · los 3 trinquetes · 1.463 tests en las suites tocadas. - Los 2 rojos de
projection-sql.test.tsson 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/sync→ 404. 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 paraoffset=0yoffset=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
/healthverde NO basta: pasó de verdadLa primera vez que se desplegó esto,
/lakehouse/healthdaba 200,/lakehouse/syncdaba
404 (o sea, era el build nuevo), el servicio estaba perfectamente arrancado… y
POST /lakehouse/ingestdevolví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/ingestseguí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 ytscno mira Python. Había tres casos así (_aiter_ndjson,
_row_id,offset/nen 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 deundefined nameenml-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_tableya no declaraSortOrdersobre__row_index. Ésa era la punta de la
cadena: DuckDB rehúsa escribir en tablas ORDENADAS → toda tabla nativa declaraba SortOrder →
INSERTyUPDATEeran 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.tssobre 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:
| Superficie | Respuesta |
|---|---|
| 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.stream | 410 ingest_stream_closed |
| Editar / borrar una fila | 409 row_not_addressable |
| Expandir el grafo por la ruta PG | 410 graph_expand_closed |
kuzu-service hydrator · apply-schema file-backed · snapshot de checkpoint | cerrados |
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
20261228aterrizó bien: esos 5.193 objetos teníanproperties = '{}'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ón | Por qué esperaba |
|---|---|
20261257 | read-router.ts y write-router.ts consultaban lakehouse_read_flags/write_flags vía makeFlagLoader en cada lectura y cada escritura |
20261259 | dataset_branch_rows se usaba en 8 sitios, incluidos junction.ts (la puerta) y lib/branches/queries.ts |
20261256 | 3 rutas vivas de sdk/v1/…/transactions llamaban esas RPC |
20261255 | el 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
20261259antes que20261257, y20261256antes que20261259: PL/pgSQL resuelve las
tablas en ejecución, así que soltardataset_branch_rowsmientras 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 elDROPsigue 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
- Next —
git revertdel merge11bf08dy push. Vercel redespliega. - ml-runner — rollback al deploy anterior en Railway.
- Las migraciones NO se revierten sueltas.
20261228y20261258son 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.