Published

🌲 Warehouse · W1 — Árbol re-enraizado + Catalog/Schema creables (Approach)

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

🌲 Warehouse · W1 — Árbol re-enraizado + Catalog/Schema creables (Approach)

Approach de la Fase W1. Padre: warehouse-reorg.md. Substrato W0 (aplicado): catalogs · schemas · datasets.schema_id sembradas (../../supabase/migrations/20261231_warehouse_catalog_hierarchy.sql).

Objetivo W1: que el Warehouse se vea y se opere como un catálogo real — el árbol pasa de My organization → project → dataset (re-proyección de carpetas) a Catalog → Schema → Dataset/Volume leído del modelo nuevo, con acciones Create catalog / Create schema. Los media sets dejan de esconderse y aparecen como Volumes.


1. El corte de riesgo — LÓGICO ahora, FÍSICO después

La bala W1 del doc padre decía "namespace Iceberg → catalog.schema". Eso se parte en dos y solo la mitad entra en W1:

  • W1 = re-enraizado LÓGICO. catalog.schema es una jerarquía en PG que ordena y gobierna. El árbol, los breadcrumbs y las CRUD leen/escriben este modelo.
  • DIFERIDO (W1.5) = re-enraizado FÍSICO. Mover las tablas Iceberg vivas de datasets.ds_{id} a {catalog}.{schema}.ds_{id} en Lakekeeper es invasivo (renombra namespaces con snapshots en vuelo, toca los routers de lectura/escritura). Se hace aparte, gated y coordinado con el write-path nativo (fase 2d). En W1 el físico sigue datasets.ds_{id}; el schema_id es overlay lógico.

Fuera de W1 también: el swap del picker "output folder" → "catalog.schema" en Data Connection (W3), el modelo GRANT (W5), credentials/connections (W3).


2. Lectura — re-enraizar GET /api/dataspace/catalog

Hoy (app/api/dataspace/catalog/route.ts) agrupa datasets por project_id sobre user_projects y devuelve { projects, orphans, shares }. Auth: withRLSContext + requirePermission('view') + supabaseRLS.

W1 lee del modelo nuevo y devuelve una jerarquía de 3 niveles, separando Dataset (tabla) de Volume (service='media_set'):

{
  "workspaceId": "…",
  "catalogs": [{
    "id": "…", "name": "main", "comment": "…",
    "schemas": [{
      "id": "…", "name": "Ventas", "comment": "…",
      "datasets": [{ "id","name","display_name","status","row_count","column_count","size_bytes","file_id","updated_at" }],
      "volumes":  [{ "id","name","display_name","status","size_bytes","file_id","updated_at" }]
    }]
  }],
  "shares": [ /* sin cambios: dataspace_transfers direction='in' */ ]
}

Query (3 fetches workspace-scoped, mismo patrón):

  • catalogs (id, name, comment) del workspace, order by name.
  • schemas (id, name, comment, catalog_id) del workspace, order by name.
  • datasets (+ schema_id, service) del workspace.

Ensamblado:

  1. Split: service='media_set'volumes; resto → datasets.
  2. Group by schema_id → cuelga de su schema; group schemas by catalog_id.
  3. Catálogos/schemas vacíos SE MUESTRAN (como Databricks: un Create schema produce un nodo visible aunque no tenga datos aún). ← quita el .filter(datasets.length>0) actual.
  4. Huérfanos (schema_id NULL): fallback defensivo — si tienen project_id, resolver por schemas.legacy_project_id; si no, al schema default del main (ver §5, el trigger normalmente ya lo evita).

Consumidor único: WarehouseTab. Se cambian ruta y componente en el mismo PR (pre-release, sin flag). El shape viejo (projects/orphans) se retira.


3. Escritura — que el modelo se auto-mantenga (invariantes por trigger)

El riesgo real de W1 no es la lectura, es la deriva: workspaces/projects/datasets creados después de la siembra W0 no tendrían catalog/schema/schema_id. En vez de parchear cada create-path en la app, se mantienen los invariantes en el borde de datos (triggers) — robusto y con mínimo churn de código. Migración auxiliar 20261232_warehouse_w1_invariants.sql:

#TriggerEfecto
T1AFTER INSERT ON workspacescrea el catalog main + schema default del nuevo workspace
T2AFTER INSERT ON user_projectscrea un schema (en el main del workspace) con legacy_project_id = NEW.id
T3BEFORE INSERT OR UPDATE ON datasetssi schema_id IS NULL: lo deriva de project_id vía schemas.legacy_project_id; si no hay project → schema default

Más, en la misma migración: seed del schema default para los main existentes + backfill de datasets huérfanos (schema_id NULL) → su default.

Notas de RLS/seguridad: T1 corre en el contexto del webhook de Clerk (service_role, bypassa RLS). T2/T3 corren como el usuario autenticado, que es miembro del workspace → las policies … IN (SELECT user_workspace_ids()) pasan sin SECURITY DEFINER. T3 solo hace un SELECT de resolución + set de columna (sin insert cruzado).

Trade-off: los triggers son "magia implícita". Son deliberados para la fase-puente; se retiran cuando el picker se vuelve explícito (catalog.schema en W3) y schema_id se setea en el create-path.

Create catalog / Create schema (rutas nuevas)

Mismo patrón que el read (requirePermission de mutación + supabaseRLS; la RLS de W0 hace el resto):

POST /api/dataspace/catalogs   { name, comment? }            → 201 { id, name, comment }
POST /api/dataspace/schemas    { catalogId, name, comment? } → 201 { id, name, comment }
  • Gate: el mismo de los creates workspace-scoped existentes (verificar el string en lib/graphs/auth — probablemente requirePermission('edit')).
  • workspace_id se deriva del ctx (no del body). Para schemas, workspace_id = el del catalog (validar que el catalogId es del workspace del ctx antes de insertar).
  • Rename/Delete de catalog/schema → W1.1 (no bloqueante para "se ve como Databricks").

Decisión de routing: las rutas nuevas se quedan bajo /api/dataspace/* por consistencia con catalog/item/sample (Capa B). Un rename cosmético del folder a /api/warehouse/* es un pase aparte de bajo valor; no en W1.


4. UI — WarehouseTab gana el nivel Catalog

Hoy: My organization → project → dataset. W1:

▾ main                         (Catalog)      ← icono catálogo
   ▾ Ventas                    (Schema)       ← icono database
        ▦ transacciones        (Dataset)      ← icono table
        ▦ clientes             (Dataset)
        ▾ Volumes                             ← agrupación (o inline con icono volume)
             ▣ media_reviews   (Volume)       ← service='media_set'
▾ Shares received              (sin cambios)
  • Interfaces: Catalog { id, name, comment, schemas: Schema[] }, Schema { id, name, comment, datasets: DsItem[], volumes: DsItem[] }. Se retiran CatalogProject/orphans.
  • Expand/colapso a 3 niveles (hoy 2). Auto-expandir el primer catalog + su primer schema.
  • Botón + "Create" (como el menú Create de Databricks): "Create catalog" / "Create schema" → POST nuevos → refetch. (Add data / Upload to volume / Create credential → placeholders para fases futuras.)
  • Iconos: catalog cloud-server/database, schema database/folder, dataset th, volume folder-close/media.
  • Breadcrumb del detalle: catalog › schema › dataset (hoy Catalog Explorer › namespace › item).
  • Detalle (ItemDetail): sin cambios de fondo (sigue por datasetId vía /api/dataspace/item); solo el breadcrumb y, para volumes, ocultar tabs no aplicables (Schema/Sample siguen; History/Quality según aplique).

5. Migración auxiliar 20261232_warehouse_w1_invariants.sql

Contenido (idempotente + rollback en header):

  1. Schema default por cada catalog main existente (ON CONFLICT (catalog_id,name) DO NOTHING).
  2. Backfill huérfanos: UPDATE datasets SET schema_id = <default del main del workspace> WHERE schema_id IS NULL.
  3. T1/T2/T3 (funciones plpgsql + triggers) de §3.
  4. NOTIFY pgrst, 'reload schema';

Aditiva e inerte respecto a la lógica de negocio; solo garantiza invariantes. Se puede aplicar antes que el código de §2–§4 sin romper nada (los triggers empiezan a mantener el modelo; el read viejo lo ignora).


6. Riesgos y mitigaciones

RiesgoMitigación
Datasets nuevos post-W0 sin schema_id → caen a "huérfano"T3 los deriva de project_id en el borde; el read tiene fallback por legacy_project_id.
Workspaces nuevos sin catalogT1 siembra main+default en el insert.
Nombres de schema duplicados (dos projects "Sales")Ya desambiguado en W0; T2 replica la desambiguación.
Media sets aparecen dos veces (Dataset y Volume)Split explícito por service='media_set'; excluir del array datasets.
Confundir logical vs físicoNamespace Iceberg no se toca en W1 (§1); documentado.
Permiso de mutación incorrecto en las CRUDVerificar el gate contra un create workspace-scoped existente antes de mergear.

7. Desglose de tareas (orden)

  1. Migración 20261232 (T1/T2/T3 + default schema + backfill) — escrita, pendiente aplicar. (DB)
  2. Read pathcatalog/route.ts al shape v2 (catalogs→schemas→datasets/volumes; muestra vacíos; split media; fallback huérfanos→default). (API)
  3. CRUDPOST /api/dataspace/catalogs y /schemas (validación de tenancy + 409 en duplicado). (API)
  4. WarehouseTab — interfaces nuevas, árbol de 3 niveles, create inline (catalog/schema), iconos (database/folder/th/box), breadcrumb catalog › schema › item. (UI)
  5. Verificación (§8) — pendiente (typecheck + preview autenticado).
  6. (W1.1 opcional) rename/delete de catalog/schema — no hecho.

8. Verificación

  • El árbol muestra main → schemas → datasets/volumes; schemas/catalogs vacíos visibles.
  • "Create catalog" y "Create schema" crean el nodo y persisten (aparece tras refetch; visible solo dentro del workspace por RLS).
  • Un dataset sincronizado nuevo (Data Connection) aparece bajo el schema de su project (T3), no en huérfanos.
  • Un media set aparece como Volume, no como Dataset, y no duplicado.
  • preview_*: cargar el Warehouse, expandir a 3 niveles, screenshot; preview_network sobre /api/dataspace/catalog (shape v2) y sobre los POST de create.

9. Decisiones abiertas (W1)

  • Nodo raíz del árbol: ¿"My organization" (workspace) como raíz con catalogs debajo, o los catalogs directos? (Databricks: catalogs directos bajo la conexión.)
  • Volumes en el árbol: ¿sub-grupo "Volumes" por schema, o inline con icono distinto?
  • ¿T2 para siempre? El acople project→schema vía trigger es puente; decidir cuándo los projects dejan de crear schemas (cuando el picker sea catalog.schema, W3).