Files
fn_registry/python/functions/infra/pg_upsert.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

6.2 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
pg_upsert function py infra 1.0.0 impure def pg_upsert(dsn: str, table: str, rows: list[dict], key_cols: list[str], update_cols: list[str] = None) -> dict UPSERT idempotente en lote en una tabla PostgreSQL con ownership selectivo de columnas. Construye INSERT INTO <table> (cols) VALUES %s ON CONFLICT (key_cols) DO UPDATE SET col = EXCLUDED.col, ... (o DO NOTHING) y lo ejecuta con psycopg2.extras.execute_values. update_cols=None actualiza todas menos key_cols; update_cols=[] hace DO NOTHING; lista explicita = ownership selectivo (las no listadas conservan su valor). Distingue insert vs update via el pseudo-columna xmax (RETURNING (xmax = 0) AS inserted). Valida que table y columnas casen ^[A-Za-z_][A-Za-z0-9_]*$ antes de interpolarlas; los valores van por placeholders. Commit al exito, rollback al fallo, cierre en try/finally. Devuelve {status:'ok', inserted, updated} o {status:'error', error} sin lanzar. Espejo de duckdb_upsert para Postgres. key_cols deben tener PRIMARY KEY o UNIQUE. Depende de psycopg2 (2.9.x en python/.venv).
postgres
postgresql
sql
upsert
idempotent
infra
false error_py_core
re
psycopg2
name desc
dsn Cadena de conexion PostgreSQL en formato postgresql://user:pass@host:port/dbname.
name desc
table Nombre de la tabla destino. Validado como identificador SQL [A-Za-z_][A-Za-z0-9_]*; un nombre raro devuelve {status:'error'}. La tabla debe existir y key_cols debe tener PRIMARY KEY o UNIQUE.
name desc
rows Lista de dicts, un dict por fila (clave = nombre de columna). El esquema de insercion lo fija la PRIMERA fila; todas deben tener exactamente las mismas claves o se devuelve error. Lista vacia -> {status:'ok', inserted:0, updated:0}.
name desc
key_cols Columnas de la clave de conflicto (no vacia). Deben existir como PRIMARY KEY o UNIQUE en la tabla y estar presentes en las claves de cada fila.
name desc
update_cols Columnas a actualizar en conflicto. None (default) = todas menos key_cols. [] = DO NOTHING (inserta nuevas, no toca existentes). Lista = DO UPDATE SET solo esas (ownership selectivo: las no listadas conservan su valor previo).
dict. En exito: {status:'ok', inserted:int, updated:int} (inserted = filas con xmax=0 en RETURNING, updated = filas en conflicto actualizadas). Con DO NOTHING las filas en conflicto no se devuelven por RETURNING y no cuentan en ninguno. En error (sin lanzar): {status:'error', error:str}. true
test_skip_sin_pg_test_dsn
test_identificador_invalido_devuelve_status_error
test_inserta_filas_nuevas_cuenta_inserted
test_conflicto_actualiza_y_cuenta_updated
test_ownership_selectivo_no_pisa_columna_excluida
test_do_nothing_no_actualiza
python/functions/infra/pg_upsert_test.py python/functions/infra/pg_upsert.py

Ejemplo

import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from infra.pg_upsert import pg_upsert

dsn = "postgresql://user:pass@localhost:5433/trends"
# La tabla leads(email PRIMARY KEY, name TEXT, score INT) ya existe.

# Re-ingest 1: inserta el lead.
print(pg_upsert(
    dsn, "leads",
    [{"email": "ana@x.com", "name": "Ana", "score": 0}],
    key_cols=["email"],
))
# {'status': 'ok', 'inserted': 1, 'updated': 0}

# Re-ingest 2: el feed trae name actualizado y score=0 (default del feed),
# pero solo autorizamos actualizar 'name'. 'score' lo posee la DB y NO se pisa.
print(pg_upsert(
    dsn, "leads",
    [{"email": "ana@x.com", "name": "Ana Lopez", "score": 0}],
    key_cols=["email"],
    update_cols=["name"],
))
# {'status': 'ok', 'inserted': 0, 'updated': 1}

Cuando usarla

Usala cuando un re-ingest periodico no debe pisar campos que ya posee la DB: pasa update_cols SIN esos campos (ownership selectivo). Tipico en pipelines de ingesta idempotente (catalogo, leads, precios competencia, entidades OSINT) donde una fila se reinserta y ciertas columnas se enriquecieron despues (score calculado, anotacion manual, flag derivado) y deben sobrevivir al refresco. update_cols=None para un upsert "todo" clasico, update_cols=[] para insertar solo filas nuevas. Es el espejo de duckdb_upsert para Postgres. Para append-only puro usa pg_insert_rows.

Gotchas

  • Escritura real en disco (impura). ON CONFLICT (key_cols) solo funciona si esas columnas tienen PRIMARY KEY o UNIQUE en la tabla; sin esa restriccion Postgres lanza error y vuelve como {status:'error', ...}. La tabla debe existir de antemano (la funcion NO la crea — usa pg_create_table_from_rows).
  • Fiabilidad de inserted/updated: el conteo usa el pseudo-columna del sistema xmax (RETURNING (xmax = 0)). Es la tecnica estandar y fiable en el caso normal (single-writer, sin triggers raros): xmax = 0 = INSERT puro, xmax != 0 = UPDATE por conflicto. Caveats conocidos: (1) con update_cols=[] (DO NOTHING) las filas en conflicto NO se devuelven por RETURNING, asi que ni cuentan como insert ni como update — solo se reportan las filas nuevas en inserted; (2) si la tabla tiene BEFORE INSERT/UPDATE triggers, REPLICA IDENTITY o subtransacciones que tocan la fila, el valor de xmax puede no ser 0 en un insert real y desviar el conteo.
  • Inyeccion SQL: table y los nombres de columna se validan contra ^[A-Za-z_][A-Za-z0-9_]*$ antes de interpolarlos (no se pueden parametrizar identificadores). Un nombre con espacios, comillas, puntos o vacio devuelve {status:'error'}. Los valores de las filas siempre van por los placeholders de execute_values.
  • Esquema fijo por la primera fila: el conjunto de columnas de insercion lo determina rows[0]. Todas las filas deben tener exactamente las mismas claves; si una difiere, se devuelve error (no se hace insercion parcial).
  • Single-statement por lote: todo el lote va en un solo INSERT ... VALUES %s dentro de una transaccion. Si una fila viola una constraint (FK, NOT NULL en una columna ausente), Postgres aborta el lote entero y se hace rollback.
  • Nunca lanza: DSN invalido, tabla sin UNIQUE, tipo invalido o falta de psycopg2 vuelven como {status:'error', error:str}.