Published

🔌 Registro de Migración de Puertos — al Data Space paradigm

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

🔌 Registro de Migración de Puertos — al Data Space paradigm

Tracker vivo. El objetivo: cada building block / recurso de Carbon deja de hablar PG crudo y pasa a ser un enchufe del Data Space vĂ­a la facade loadItemForConsumption (dataspace-router.md). Approach por fases: source-of-truth.md.

Estados: FACADE = usa loadItemForConsumption (paradigma pleno) · SEAM = usa resolveDatasetRowSource/Sink (enchufe a nivel seam, correcto pero no facade) · DORMANT = cableado al seam, executor Iceberg listo, en modo gated · NO-EXEC = registrado en el seam pero sin executor Iceberg → siempre PG · RAW-PG = habla PG crudo, bypass del Data Space · N/A = no aplica (file-backed / control-plane).

Actualizado 2026-07-09. 2c CERRADO — todos los readers analytics (incl. los de baja prio y los de live-SQL) rutan por la facade; el camino queda PLANO para el flip global de pipeline.output (2d). 2d.1/2d.2 HECHOS: el brazo iceberg del Apply-path (transform/join/split) + el deploy replace-puro escriben el tridente nativo; canary VERDE E2E (scripts/dataspaces/pipeline-output-canary.ts, npm run canary:pipeline-output, en main.test, paridad PG↔Iceberg). SIGUIENTE = 2d.3 / Tier B — ver 2d3-tier-b-approach.md.


Lectura (readers)

Puerto (readerId)ConsumidorEstadoNota / siguiente
dataspace.previewInterfaz Data Space (components/workspace/tabs/dataspace)✅ FACADEitem/sample routes → loadItemForConsumption. Nuevo (2026-07-03).
export.streamTable export (snapshot read)âś… FACADEMigrado 2026-07-03 (run-export.ts streamRows). Delta/ledger sigue RAW-PG (sin equivalente en contract).
objects.syncObject sync / bulk-create✅ FACADEMigrado 2026-07-03 → read({ afterRowIndex, limit, includeIdentity }) keyset. El gate de identidad (Iceberg sin __row_id/__row_index → PG) pasó del puerto al ROUTER: la facade cae a PG si se pide includeIdentity sobre un Iceberg no identity-ready.
datasets.browseDatasetTab / DataAssetView / ExploreSource (hub de lectura)✅ FACADEMigrado 2026-07-03 (app/api/datasets/route.ts includeRows). Browse sin filtro → loadItemForConsumption(...).read() (keyset en ambos modos cursor/offset, PG↔Iceberg unificado; __created_at añadido al fallback PG de la facade para paridad con Iceberg). El filtro substring (data::text ILIKE) NO tiene equivalente en la contract → se queda en el seam→PG (isla RAW-PG catalogada, como el delta de export.stream). STRICT: hoy resuelve PG salvo flip + caught-up.
datasets.rows.lookupLookup de fila por id🟡 DORMANT (STRICT)Executor icebergRowIdLookup listo.
graph.explorer.dataGraph explorer (nodos/edges)✅ FACADEMigrado 2026-07-03. Muestreo de nodos (read keyset) + aristas (read acotado) → loadItemForConsumption. Sin filtro → sin delta. Ruta ya no toca dataset_rows (consumidor puro).
graph.explorer.searchGraph explorer (faceted search)✅ FACADEMigrado 2026-07-03 → read({ filters:[contains] }). El substring por columna gana pushdown a PG (data->>col ILIKE) en el fallback de la facade → sin regresión de latencia; en Iceberg va al row_filter.
graph.explorer.valuesGraph explorer (distinct values)✅ FACADEMigrado 2026-07-03 → aggregate({ contains }). El q tecleado con pushdown a PG; el lado PG converge a la semántica top-N-by-count de Iceberg (antes muestreo de 500).
graph.kuzuKuzu graph hydration✅ FACADEMigrado 2026-07-03 (2c). TODOS los reads (nodos + aristas, ambas hydrate fns) → kuzuFacadeRead (loadItemForConsumption). Los IR filters se mapean a filtros estructurados (pushdown); el regex es post-filtro JS. Ruta ya no toca dataset_rows (salió del ratchet).
dashboards.aggregateDashboards (AGGREGATE + QUERY paths)✅ FACADEREFERENCE migrado 2026-07-03 (native → stream() + aggregateInMemory). QUERY multi-dataset CERRADO 2026-07-04 (2c): executeQueryDataset resuelve cada dataset por la facade vía hydrate-to-inline-jsonb (lib/lakehouse/sql-source-hydration.ts) — un nativo se hidrata a jsonb_to_recordset (función PURA → corre dentro del pool READ ONLY, sin relajar el sandbox); PG queda el default no-op. El literal dataset_rows persiste como el path PG (allowlisted).
sql-editor.querySQL Editor internal mode (app/api/sql-editor/execute/route.ts)✅ FACADEMigrado 2026-07-04 (2c). Live-SQL CTE de usuario (READ ONLY) → mismo hydrate-to-inline-jsonb que dashboards QUERY; reader nuevo sql-editor.query en el registry. El literal dataset_rows persiste como el path PG (allowlisted; sandbox a nivel DB-role).
studio.digest / studio.training_pairsAI Studio samplers🟢 SEAM (EVENTUAL)flip-ready.
datasets.export.legacyCSV export legacy✅ FACADEMigrado 2026-07-04 (2c). stream() (snapshot completo, mismo estado que browse); el filtro por-transacción legacy lo subsume la facade (lee el estado actual — snapshot-replace borra las filas viejas).
sdk.rowsSDK v1 rows API + notebooks (Jupyter read_table)✅ FACADEMigrado 2026-07-03. Los notebooks NO tienen reader propio → leen por este puerto (la capa Jupyter es solo binding). JSON→read({offset,limit}), NDJSON→stream(). Cerró un bypass RAW-PG oculto (.from(sourceTable) con nombre en variable → evadía el ratchet). El overlay de branch lo resuelve el ROUTER vía el contrato v1.1 (ItemRef.branchId → dataset_branch_rows, plano PG), no una isla.
sdk.files.listSDK v1 file manifest⚪ N/A (control-plane)Lee el manifiesto de un dataset content_type=file (nombres + signed-URLs), no filas tabulares → nunca va a Iceberg, PG por diseño (como el writer dataset.manifest).
model.schema.inferSchema API (app/api/model/schema/route.ts)✅ FACADEMigrado 2026-07-04 (2c). Sample read({limit:25}) + preview paginado read({offset,limit}) (v1.1) + legacy read({limit:500}); el count = descriptor rowCount (datasets.row_count, canónico) → arregla el count=0 latente de un nativo con dataset_rows drenado. Pseudo-dataset (Strategy B6) → handle null → fallback a schema metadata como antes.
pipeline.expectationsData-quality checks (post-write)✅ FACADEMigrado 2026-07-04 (P0.1). Las 4 checks resuelven la fuente por resolveSqlSource (hydrate-to-inline-jsonb, _shared.resolveExpectationSource); PG hoy vía ctx.query (read-your-writes en la tx del materialise), un output nativo hidrata la snapshot → ya no valida en falso sobre dataset_rows drenado.
mediaset.itemmedia-set item🔴 NO-EXECregistrado sin executor → PG.
pipeline.source (sourceCte)Pipelines compute (input)✅ FACADE (read)LINCHPIN CERRADO 2026-07-03. sourceCte.ts ya no hardcodea FROM dataset_rows — la fuente se resuelve por la facade (source-resolver.ts → loadItemForConsumption) en los 4 call-sites (transform/join/split/runtime). Hoy PG no-op (mode=off); un source Iceberg-nativo se hidrata a una temp (misma tx, Opción A) y el SQL corre idéntico. Karma (Opción B) confirmado diferido. Salió del ratchet (22→21). El WRITE del pipeline (INSERT de salida) sigue PG → paso 6.
model.objects.grid (objects_resolved)Objects grid✅ DESACOPLADO (seguro)La vista objects_resolved ya NO hace LEFT JOIN dataset_rows (mig. 20261228) — lee objects.base_properties denormalizado (Track A de objects.sync). SEGURO para datasets nativos sin migración (base_properties vive en PG). Ya NO es el coupling #1.
—ML training↪️ externoml-runner Python resuelve su tier (snapshot ref).

Escritura (writers)

Puerto (writerId)ProductorEstadoNota / siguiente
sync.full · sync.incremental · sync.appendData Gateway ingest🟢 SEAM (iceberg_native default)Ya nativo R2 por defecto.
cdc.appendCDC syncs (4 conectores)🟢 SEAM (iceberg_native default)Ya enchufado (cdc/sink.ts). Append-only. Facade .write() espera paso 6.
(native)EDC landing🟢 nativowriteIcebergNativeFromParquet siempre R2.
(facade native)Facade write() (rows/parquet)âś… FACADE (solo iceberg)Verificado E2E. PG-sink LANZA (frontera del paso 6).
pipeline.outputPipeline / model outputs (deploy materialiser + model-node)✅ SEAM (nativo espectro completo, defaultMode pg)2d.1/2d.2/2d.3 CERRADOS. El deploy materialiser + el model-node escriben el tridente nativo en TODO el espectro: primer-deploy (replace) + re-deploy (upsert/append/append_new anti-join) + delta (incremental) + model-node (buffer+single-replace + __source_row_index__). defaultMode=pg → opt-in por-dataset (canary VERDE E2E, npm run canary:pipeline-output). Diferido a Karma/Fase 5: read nativo de la ventana delta, unificación correlación model. Docs: 2d3-tier-b-approach.md (+ b4/b5).
— (transform-paths)transform/join/split/runtime outputs✅ SEAM (Apply-path nativo, 2d.1b/c)Los INSERT van por resolveDatasetRowSink('pipeline.output'). El brazo iceberg del Apply-path completo (transform/join/split) escribe el tridente vía writeOutputToTrident (replace; split dual-target = 2 writes nativos). Flippable por-dataset; PG default. La procedencia se propaga (Fase D).
manual.tableTabla manual✅ nativo (default)Paso 6 · 2a (2026-07-03): defaultMode=iceberg_native. La ruta escribe writeIcebergNativeSnapshot(replace). Hereda el gate system-wide objects_resolved (2c); reversible + pinnable.
ingest.stream · row.editstreaming / edición punto🔴 RAW-PG (diferido → Karma)sin primitiva Iceberg (streaming open/commit, point-DELETE) → 2e/Karma. Clamp PG.
dataset.manifestMedia sets (manifiesto)⚪ N/A (PG por diseño)file-backed; los manifiestos son control-plane, NO van a Iceberg.

Los dos tracks

  • READERS → migran LIMPIO a loadItemForConsumption(...).read()/.stream() (la facade tiene lectura completa con fallback PG). Ola completa: datasets.browse âś… → graph.explorer.* âś… → notebooks/SDK (sdk.rows) âś… → objects.sync âś… → graph.kuzu âś… → dashboards.aggregate REFERENCE âś… → datasets.export.legacy âś… → model.schema.infer âś… → sql-editor + dashboards.aggregate QUERY (live-SQL → hydrate-to-inline-jsonb) âś…. 2c cerrado. Pendientes menores fuera de 2c: datasets.rows.lookup (DORMANT) + dar executor a los NO-EXEC (mediaset.item, pipeline.expectations). Cada migraciĂłn es reversible (canary-gated).
    • Los readers de LIVE-SQL (sql-editor, dashboards.aggregate QUERY) NO usan read() estructurado — corren SQL arbitrario de usuario. Su enchufe = hydrate-to-inline-jsonb (lib/lakehouse/sql-source-hydration.ts): cada dataset referenciado se resuelve por la facade y, si es nativo, sus filas se hidratan a un array JSON servido vĂ­a jsonb_to_recordset($n::jsonb) — una FUNCIĂ“N PURA, asĂ­ que la CTE corre idĂ©ntica y DENTRO de BEGIN READ ONLY (no hace falta CREATE TEMP TABLE, prohibido en read-only → sin relajar el sandbox de SQL arbitrario, sin nueva superficie de escritura). Hermano interactivo del linchpin source-resolver.ts (que hidrata a temp en la tx read-write del pipeline). Inert-but-ready: hoy PG no-op; la rama hydrate se activa por-dataset al flipear (canary).
    • El contrato es v1 y se ENRIQUECE por demanda (no se fuerza a los building blocks por un router rĂ­gido). Extensiones ya en la piedra: (a) pushdown de substring por columna en el fallback PG (data->>col ILIKE, antes JS) → readers interactivos filtrados sin regresiĂłn; (b) v1.1 ItemRef.branchId (overlay Lazy-COW dataset_branch_rows, plano PG, herencia transparente) + PageQuery.offset (paginaciĂłn posicional, nativa en PG / emulada en Iceberg) + descriptor auto-descrito (rowCount/currentTransactionId/branchId) — habilitĂł sdk.rows sin islas; (c) gate de identidad en el router: includeIdentity sobre un Iceberg no identity-ready cae a PG (que sĂ­ lleva id/row_index) — habilitĂł objects.sync moviendo el gate del puerto a la piedra.
    • Enriquecimientos PENDIENTES identificados (para migrar el resto sin islas): sample({stride|reservoir}) — muestreo representativo (studio.digest/training_pairs, model.schema.infer); count(filters?) — conteo (model.schema.infer). El muestreo por stride (row_index % n) es nativo en PG; en Iceberg necesita soporte de ml-runner (modulo filter) → decisiĂłn de diseño abierta.
  • WRITERS → PASO 6 = encaminar la escritura al tridente. Approach fundado: paso6-write-approach.md (stack de 3 tiers · el reframe "la escritura no adelanta a la lectura" · secuenciaciĂłn). Estado: Tier-1 (sync/cdc/EDC/ingest.api) nativo · manual.table nativo (2a âś…) · Fase D propagada (2b âś…) · pipeline.output gated en objects_resolved (2c→2d) · ingest.stream/row.edit diferidos a Karma (2e).

Blockers de fondo (no por-puerto)

  • sourceCte (linchpin): âś… CERRADO (2026-07-03) — la fuente del pipeline se resuelve por la facade (source-resolver.ts, no-op PG hoy + hydrate-to-temp para nativos). El Ăşnico FROM dataset_rows hardcodeado ya no existe. Queda el WRITE (abajo) = paso 6.
  • paso 6 — pipeline WRITE (era "fix read desbloquea write", refinado): el pipeline es UNA sentencia atĂłmica WITH…INSERT INTO dataset_rows; el read ya es facade-routed, pero el output INSERT (materialiser pipeline.output maxMode=pg + la fuga directa de transform-paths transform/join/split) sigue PG. Dirigirlo al data space = construir la estrategia de write nativo (compute PG → facade.write()/ml-runner → Iceberg), rompiendo la sentencia atĂłmica. No necesita Karma (ml-runner ya escribe Iceberg); Karma lo subsume despuĂ©s. Investigar el espectro de Fase 2.
  • transform-paths: cerrar la fuga de INSERT INTO dataset_rows directo (rutar por el write-seam) — parte del paso 6 arriba.
  • Fase D (gobernanza): âś… CABLEADA (2026-07-03) — mergeUpstreamProvenance propagado en los chokepoints de derivaciĂłn (createOutputDataset del pipeline output + createTransformOutputDataset + createJoinOutputDataset). El output hereda la procedencia del/los source(s) (counterparty/edc/upstream). Pendiente menor: split branches + object-type + model-node (mismo patrĂłn).

Tracker vivo — actualizar al migrar cada puerto.