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

101 lines
6.2 KiB
Markdown

---
name: pg_upsert
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def pg_upsert(dsn: str, table: str, rows: list[dict], key_cols: list[str], update_cols: list[str] = None) -> dict"
description: "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)."
tags: [postgres, postgresql, sql, upsert, idempotent, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_py_core"
imports: [re, psycopg2]
params:
- name: dsn
desc: "Cadena de conexion PostgreSQL en formato postgresql://user:pass@host:port/dbname."
- name: table
desc: "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: rows
desc: "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: key_cols
desc: "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: update_cols
desc: "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)."
output: "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}."
tested: true
tests: ["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"]
test_file_path: "python/functions/infra/pg_upsert_test.py"
file_path: "python/functions/infra/pg_upsert.py"
---
## Ejemplo
```python
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}`.