---
name: dedup_duckdb_table_by_hash
kind: pipeline
lang: py
domain: pipelines
purity: impure
version: "1.0.0"
signature: "def dedup_duckdb_table_by_hash(duckdb_path: str, table: str, exclude_cols: list[str] | None = None) -> dict"
description: "Elimina filas duplicadas de una tabla DuckDB calculando un md5 de las columnas de datos. Anade columna row_hash idempotentemente, actualiza hashes nulos y borra duplicados conservando la primera insercion por rowid."
tags: [dedup, duckdb, transformer, pipeline, dataops]
uses_functions: [cdp_extract_recipe_py_pipelines]
uses_types: []
returns: []
returns_optional: false
error_type: error_go_core
imports: [duckdb]
tested: true
tests:
  - "dedup elimina filas duplicadas y conserva unicas"
test_file_path: "python/functions/pipelines/dedup_duckdb_table_by_hash_test.py"
file_path: "python/functions/pipelines/dedup_duckdb_table_by_hash.py"
params:
  - name: duckdb_path
    desc: "Ruta DuckDB file (absoluta o relativa a FN_REGISTRY_ROOT)."
  - name: table
    desc: "Nombre tabla a deduplicar."
  - name: exclude_cols
    desc: "Cols a excluir del hash (metadata como run_id, extracted_at, row_hash). None usa default [run_id, extracted_at, row_hash]."
output: "dict {status, rows_before, rows_after, dedup_removed, duration_ms, hash_column}"
---

## Ejemplo

```python
from pipelines.dedup_duckdb_table_by_hash import dedup_duckdb_table_by_hash

r = dedup_duckdb_table_by_hash("apps/data_factory/data/hn_top_stories.duckdb", "hn_stories")
print(r)
# {"status": "ok", "rows_before": 120, "rows_after": 30, "dedup_removed": 90, "duration_ms": 45, "hash_column": "row_hash"}
```

CLI directo:

```bash
$HOME/fn_registry/python/.venv/bin/python3 \
  python/functions/pipelines/dedup_duckdb_table_by_hash.py \
  apps/data_factory/data/hn_top_stories.duckdb hn_stories
```

## Cuando usarla

Cuando un extractor periodico re-inserta filas iguales (mismo contenido, distinto `run_id`/`extracted_at`) y quieres deduplicar in-place sin tocar el pipeline upstream. Tipicamente como paso `transformer` despues de `cdp_extract_recipe` en un DAG de scraping.

## Gotchas

- **rowid y VACUUM**: DuckDB rowid puede recalcularse tras `VACUUM`. En esta funcion solo se usa dentro de la misma transaccion de DELETE, por lo que no hay inconsistencia practica.
- **Colisiones md5**: md5 no colisiona en practica para tablas de escala HN (miles de filas). Si la tabla crece a millones de filas con datos binarios, cambiar `md5(...)` por `sha256(...)` en el SQL.
- **Tabla inexistente**: si `<table>` no existe en el DuckDB, retorna `status=error` con mensaje descriptivo en lugar de lanzar excepcion.
- **exclude_cols case**: la comparacion de columnas excluidas es case-insensitive (`c.lower()`), pero el nombre en la query se usa tal cual lo devuelve `DESCRIBE`.
- **Primera ejecucion**: si la tabla ya tiene `row_hash` de una ejecucion anterior, solo se actualizan las filas con `row_hash IS NULL` (idempotente).
