F0 · DuckDB ↔ Lakekeeper ↔ R2 smoke
Primer paso del major DuckDB = motor de cómputo+escritura del Warehouse (detrás de Junction). Spec completo: docs/architecture/duckdbengine.md.
Objetivo de F0 (inerte, cero cambio en el editor): probar que DuckDB puede ATTACH el catálogo Lakekeeper vivo, servir un information_schema real del catálogo, y leer data de una tabla Iceberg en R2. Eso valida catalog-binding + creds R2 + motor DML-capable en nuestra infra antes de F1+.
Decisiones que encarna (§7 del spec)
- Deployment = servicio dedicado (duck-server), F1+. F0 es agnóstico → basta este script.
- Creds R2 = llaves estáticas interim. ⚠️ El credential-vending Lakekeeper→DuckDB está roto hoy (DuckDB reutiliza creds scoped a
metadata_pathpara eldata_path→ 403 en data files; PyIceberg no). Issues: duckdb-iceberg #792, #670, duckdb #19185. Por esoSECRET s3estático +ATTACH … ACCESS_DELEGATION_MODE 'none'. El vending SOTA queda diferido, gated en esos issues.
Cómo correr
Ruta recomendada — job one-off en Railway (junto a ml-runner)
Ahí ya viven los LAKEHOUSE_* y hay red al Lakekeeper. En un one-off del servicio ml-runner:
pip install duckdb pytz # duckdb ≥1.5.3 (validado con 1.5.4); pytz para columnas tz
python scripts/duckdb/f0_smoke.py
Local
Requiere DuckDB ≥1.5.3 y los ~7 secretos exportados (o en un .env que cargues antes). .env.local del repo es el spike SQLite — no trae REST URI / OIDC / llaves R2.
pip install duckdb pytz
# exporta las LAKEHOUSE_* (ver más abajo) …
python scripts/duckdb/f0_smoke.py --smoke-table lake.datasets.ds_<uuid>
Env requerido (contrato = services/ml-runner/.env.example)
| Var | Qué es |
|---|---|
LAKEHOUSE_REST_URI | endpoint Iceberg REST (…/catalog) |
LAKEHOUSE_REST_WAREHOUSE | warehouse (prod: lakehouse) |
LAKEHOUSE_CATALOG_OAUTH2_SERVER_URI | token endpoint Keycloak |
LAKEHOUSE_CATALOG_CREDENTIAL | <client-id>:<client-secret> |
LAKEHOUSE_CATALOG_SCOPE | scope OIDC |
LAKEHOUSE_S3_ENDPOINT | https://<account>.r2.cloudflarestorage.com |
LAKEHOUSE_S3_ACCESS_KEY_ID / LAKEHOUSE_S3_SECRET_ACCESS_KEY | llaves R2 estáticas |
LAKEHOUSE_S3_REGION | default auto |
SMOKE_TABLE (opc.) | FQN a contar; si falta, se autoelige la 1ª tabla |
El runner no imprime secretos (enmascara los CREATE SECRET).
Qué valida (salida esperada)
count(*)deinformation_schema.tables> 0 → catalog-binding + info_schema real (esto arregla de raíz el bugcharacter_maximum_length, F2).- Muestra de 10 tablas del catálogo vivo.
count(*)sobre 1 tablamain.default.ds_*→ lectura de data en R2 con creds estáticas.
✅ Estado: F0 VERDE (validado 2026-07-09, DuckDB 1.5.4)
Ejecutado contra el Lakekeeper de prod: ATTACH OK (OIDC keycloak), 23 tablas en el catálogo, lake.bench.ds_* → 1.500 filas y SELECT * LIMIT 5 devolvió data real de R2 — sin 403, el workaround de creds estáticas funciona. Hallazgos que ya están reflejados en el .sql/runner:
information_schemade un catálogo Iceberg REST adjunto NO existe (lake.information_schema→ error). La introspección real va porduckdb_tables()/duckdb_columns()(esto es lo que F2 usará para arreglar el bugcharacter_maximum_length).- Los namespaces de Lakekeeper se ven como schemas planos (
bench,datasets,main), nomain.default. INSTALL icu; LOAD icu;es obligatorio — las tablasds_*nativas llevan__created_at TIMESTAMP WITH TIME ZONE.- Materializar esa columna tz a Python requiere
pytz(pip install pytz). El duck-server real (F1) debe devolver Arrow, que maneja tz sin pytz — este smoke usa el cliente Python, por eso lo necesita. count(*)en Iceberg se responde por metadata → NO prueba lectura de data files. El Smoke 4 (SELECT *) es el que fuerza el read real de R2.- 403 en el Smoke 4 pese a creds estáticas = firma del bug de vending: verificar
ACCESS_DELEGATION_MODE 'none'en elATTACHy el endpoint/URL_STYLE delSECRET r2.
Siguiente tras F0 verde
F1 (read shadow en el SQL Editor internal-mode) → F2 (information_schema real) → F3 (flip read, retirar lib/warehouse/query/dialect.ts) → F4 (DML) → F5 (Junction formal). Ver spec §5.