Files
fn_registry/python/functions/pipelines/duckdb_to_postgres.md
T
egutierrez 927437a8d8 feat(infra): grupo claude-fleet — FleetView TUI + orquestacion de Claudes
Sistema FleetView para centralizar la flota de procesos Claude Code vivos en una
sola ventana kitty + tmux (socket aislado -L fleet) con un panel TUI:

- list_claude_fleet (+ tipo claude_fleet): escanea ~/.claude/sessions + goals +
  runtime, valida procesos vivos (anti-PID-reciclado), join por sessionId.
- list_resumable_claudes (+ tipo resumable_claude): sesiones cerradas reanudables.
- wrappers tmux: tmux_new_claude_window (con --resume), tmux_swap_window_into_console
  (preserva ancho del sidebar), tmux_map_claude_panes.
- launch_kittyclaude: comando entrypoint; instala atajos alt+flechas/enter/n/0/k/r,
  mouse on, remain-on-exit off; fija el ancho del sidebar con hooks.
- docs/capabilities/claude-fleet.md + entrada en el INDEX.

Incluye ademas funciones datascience en progreso (excel/duckdb/postgres) y ajustes
varios de docs e infra de otra sesion, agrupados aqui para no perderlos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:04:41 +02:00

7.1 KiB

name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, params, output, tested, tests, test_file_path, file_path
name kind lang domain version purity signature description tags uses_functions uses_types returns returns_optional error_type imports params output tested tests test_file_path file_path
duckdb_to_postgres pipeline py pipelines 1.0.0 impure def duckdb_to_postgres(duckdb_path: str, table: str, pg_dsn: str, pg_table: str = None, mode: str = 'replace', key_cols: list = None, batch_size: int = 5000) -> dict Pipeline que sincroniza una tabla DuckDB a PostgreSQL. Es lo que desbloquea que herramientas BI (Metabase, Grafana, Superset) lean datos que viven en DuckDB, porque NO hablan DuckDB nativo pero todas hablan PostgreSQL. Pasos: (a) lee el schema con duckdb_table_schema; (b) mapea tipos DuckDB->PostgreSQL (BIGINT/INTEGER->BIGINT, DOUBLE/FLOAT->DOUBLE PRECISION, VARCHAR/TEXT->TEXT, BOOLEAN->BOOLEAN, DATE->DATE, TIMESTAMP->TIMESTAMP, resto->TEXT) y genera CREATE TABLE IF NOT EXISTS con PRIMARY KEY si key_cols (DROP TABLE IF EXISTS antes si mode='replace'), aplicandolo con pg_apply_sql; (c) lee las filas con duckdb_query_readonly paginando con LIMIT/OFFSET e inserta en PostgreSQL con pg_insert_rows (add_snapshot_date=False) en lotes de batch_size, o con pg_upsert si hay key_cols y mode!='replace'. pg_upsert se importa detras de un check de import: sin el, el camino upsert no esta disponible pero replace/append funcionan. Compone funciones del registry sin reescribir su logica. Devuelve un dict sin lanzar: {status:'ok', pg_table, rows_synced, created} en exito y {status:'error', error} en fallo. Depende de duckdb (1.5.2) y psycopg2.
duckdb
postgres
etl
sync
pipeline
duckdb_table_schema_py_infra
duckdb_query_readonly_py_infra
pg_apply_sql_py_infra
pg_insert_rows_py_infra
pg_upsert_py_infra
false error_py_core
os
re
sys
tempfile
duckdb
psycopg2
name desc
duckdb_path ruta al archivo DuckDB de origen (se lee en modo read_only; debe existir).
name desc
table nombre de la tabla DuckDB a sincronizar. Validado como identificador ^[A-Za-z_][A-Za-z0-9_]*$.
name desc
pg_dsn cadena de conexion PostgreSQL, p.ej. 'postgresql://user:pass@host:5432/db'.
name desc
pg_table nombre de la tabla destino en PostgreSQL. None (default) usa el mismo nombre que `table`. Validado como identificador.
name desc
mode 'replace' (default) hace DROP TABLE IF EXISTS + CREATE + INSERT de todas las filas (snapshot completo). 'append'/'upsert' crean la tabla si no existe y luego: con key_cols usan pg_upsert (idempotente), sin key_cols hacen INSERT append-only. Otro valor devuelve {status:'error'}.
name desc
key_cols lista de columnas de la PRIMARY KEY. Se incluyen en el CREATE como PRIMARY KEY y, en modo != 'replace', habilitan el upsert idempotente. None/[] (default) = sin PK, solo INSERT. Deben existir en el schema DuckDB.
name desc
batch_size numero de filas por lote de insercion/upsert (default 5000). Debe ser un entero positivo.
dict. En exito: {status:'ok', pg_table:str, rows_synced:int, created:bool} donde rows_synced es el total de filas volcadas y created indica si se ejecuto el CREATE/DROP del schema. En error (sin lanzar): {status:'error', error:str}. true
test_map_tipos_duckdb_a_postgres
test_build_ddl_con_pk_y_drop
test_build_ddl_sin_pk_ni_drop
test_identificador_tabla_invalido
test_mode_invalido
test_replace_sincroniza_filas
test_upsert_idempotente_con_key_cols
python/functions/pipelines/duckdb_to_postgres_test.py python/functions/pipelines/duckdb_to_postgres.py

Ejemplo

import sys
sys.path.insert(0, "python/functions")
from pipelines.duckdb_to_postgres import duckdb_to_postgres

# Snapshot completo: reemplaza la tabla destino en PostgreSQL con todas las filas
# de la tabla DuckDB. Metabase/Grafana ya pueden leerla.
res = duckdb_to_postgres(
    "/tmp/almacen.duckdb",
    "ventas",
    "postgresql://captacion:****@127.0.0.1:5433/trends",
    pg_table="ventas_diario",
    mode="replace",
)
print(res)
# {'status': 'ok', 'pg_table': 'ventas_diario', 'rows_synced': 1280, 'created': True}

# Sync idempotente por clave: no duplica filas en re-ejecuciones.
res2 = duckdb_to_postgres(
    "/tmp/almacen.duckdb",
    "clientes",
    "postgresql://captacion:****@127.0.0.1:5433/trends",
    mode="upsert",
    key_cols=["id"],
)
print(res2)  # {'status': 'ok', 'pg_table': 'clientes', 'rows_synced': 540, 'created': True}

Cuando usarla

Cuando tienes datos en un archivo DuckDB y necesitas que una herramienta BI los lea: Metabase, Grafana y Superset NO hablan DuckDB nativo, pero todas hablan PostgreSQL. Es el ultimo eslabon del flujo Excel -> DuckDB -> PostgreSQL (precedido por excel_to_duckdb_py_infra). Usa mode='replace' para refrescos completos programados (un snapshot diario que recrea la tabla) y mode='upsert' + key_cols para sincronizaciones incrementales idempotentes que no duplican filas al re-ejecutar.

Gotchas

  • DuckDB es single-writer: el pipeline abre la base en read_only para leer, pero si otro proceso la tiene bloqueada en escritura con version distinta del motor, la apertura puede fallar; el error se devuelve en el dict, no se lanza.
  • El modo read_only exige que el archivo DuckDB exista: no lo crea. Un duckdb_path inexistente devuelve {status:'error', ...} ya en el paso (a).
  • Mapeo de tipos con posible perdida: el mapeo DuckDB->PostgreSQL es conservador. Tipos no contemplados (DECIMAL con escala, HUGEINT/UBIGINT de 128 bits, LIST/STRUCT/ MAP) caen a TEXT. Si el tipado fuerte importa aguas abajo (agregaciones numericas en Metabase), revisa el schema con duckdb_table_schema_py_infra y ajusta los tipos en DuckDB antes de sincronizar.
  • mode='replace' es destructivo: hace DROP TABLE IF EXISTS sobre la tabla PostgreSQL destino antes de recrearla. Cualquier dato o indice manual de esa tabla se pierde. Para sincronizaciones que deban preservar la tabla existente usa mode='append'/'upsert' (CREATE TABLE IF NOT EXISTS, sin DROP).
  • pg_upsert opcional: se importa detras de un check de import. Si pg_upsert_py_infra no esta en el entorno, mode != 'replace' con key_cols devuelve {status:'error', ...} explicando que falta; el camino replace/append (sin upsert) sigue funcionando.
  • Upsert requiere PRIMARY KEY o UNIQUE sobre las key_cols en PostgreSQL para que ON CONFLICT funcione. El pipeline crea esa PRIMARY KEY en el CREATE cuando pasas key_cols; si la tabla ya existia sin esa restriccion (mode!='replace' y tabla preexistente), el upsert fallara — recrea con mode='replace' + key_cols una vez.
  • Snapshot no transaccional entre lectura y escritura: la lectura paginada de DuckDB y la escritura a PostgreSQL no comparten transaccion. Si la tabla DuckDB cambia a mitad del volcado (otro escritor), el resultado en PostgreSQL puede mezclar estados. Sincroniza desde una base DuckDB estable (no mientras se ingesta).
  • pg_insert_rows y pg_apply_sql lanzan RuntimeError internamente; el pipeline los envuelve en try/except y convierte el fallo a {status:'error', ...}. Nunca propaga la excepcion al caller.