duck-server
DuckDB como motor de cómputo+escritura del Warehouse, detrás de Junction. Cáscara fina (FastAPI) que ATTACHea el catálogo Lakekeeper (Iceberg REST) y lee/computa sobre Iceberg en R2. Espeja el patrón de services/ml-runner. Spec: docs/architecture/duckdbengine.md.
Fase: F1 (seed read-first). El setup DuckDB↔Lakekeeper↔R2 quedó validado en F0 (scripts/duckdb).
/querybloquea DML por defecto; abrir writes es F4 (gobernado por la facade/Junction).
Endpoints
| Método | Ruta | Auth | Qué hace |
|---|---|---|---|
GET | /health | pública | 200 si el motor está adjunto; ?deep=1 sonda el catálogo. |
POST | /query | Bearer* | Ejecuta SQL → Arrow IPC (format:"json" opc.). Guard read-only (DML necesita allow_writes:true). |
GET | /tables | Bearer* | Lista tablas del catálogo (duckdb_tables()). |
GET | /config | Bearer* | MODE auto-descriptivo (warehouse, schemas, vending off). |
* Bearer solo si DUCK_SERVER_TOKEN está seteado; en F1 puede ir abierto en la red privada.
POST /query
{ "sql": "SELECT * FROM lake.datasets.ds_toydemo001 LIMIT 10",
"format": "arrow", // "arrow" (default) | "json"
"max_rows": 1000, // tope (default MAX_QUERY_ROWS=100k)
"allow_writes": false } // true → permite DML (F4)
Respuesta arrow: application/vnd.apache.arrow.stream + headers X-Row-Count, X-Truncated. La respuesta Arrow es la canónica (maneja TIMESTAMPTZ sin pytz).
Diseño (decisiones que encarna)
- Creds R2 estáticas +
ACCESS_DELEGATION_MODE 'none'— el vending Lakekeeper→DuckDB está roto (#792); ver spec §7.2. INSTALL/LOAD iceberg+httpfs+icual boot (icu obligatorio por__created_at TIMESTAMPTZ). Extensiones pre-descargadas en la imagen (Dockerfile) → boot sin egress.- Introspección por
duckdb_tables()/duckdb_columns()(un catálogo REST adjunto no exponeinformation_schema). - Una conexión serializada (lock) + queries en threadpool. Migrar a cursor-por-query cuando la concurrencia lo pida.
- Motor stateless: las tablas viven en Iceberg/R2, no en disco local → escalable/reciclable.
Deploy en Railway
Servicio dedicado (espeja karma/ml-runner), monorepo Carbon:
- Servicio "Duck" → Root Directory =
services/duck-server(Railway detecta elDockerfile). - Variables: el set
LAKEHOUSE_*(ver.env.example) — idealmente reference vars de ml-runner (${{ml-runner.LAKEHOUSE_S3_SECRET_ACCESS_KEY}}…).PORTlo inyecta Railway. - (Opcional)
DUCK_SERVER_TOKENpara exigir Bearer.
Local
pip install -r requirements.txt
# exporta las LAKEHOUSE_* (o pon un .env local — NO lo commitees)
uvicorn app.main:app --reload --port 8000
curl localhost:8000/health
curl -s localhost:8000/tables | head
Siguiente (spec §5)
F2 (information_schema real vía duckdb_columns() → arregla character_maximum_length) · F3 (flip read, retirar lib/warehouse/query/dialect.ts) · F4 (DML gobernado) · F5 (Junction formal, lib/compute/gateway.ts).