---
name: profile_database
kind: pipeline
lang: py
domain: pipelines
purity: impure
version: "1.0.0"
signature: "def profile_database(db_path: str, tables: list = None, sample: int = 5000, report_dir: str = \"reports\", write_report: bool = True, min_inclusion: float = 0.9) -> dict"
description: "Orquestador one-shot del grupo eda a nivel de BASE: perfila TODA una base DuckDB (todas las tablas o las indicadas) componiendo profile_table por tabla, infiere las relaciones FK inter-tabla por containment y construye el join graph con diagrama Mermaid. Ensambla un DatabaseProfile (resumen por tabla + TableProfiles completos + fk_candidates + join_graph) y opcionalmente emite un report markdown DB-level + JSON sidecar. Es la composicion canonica para hazme un EDA de esta base de datos y entender su esquema relacional."
tags: [eda, relations, duckdb, profiling, data-quality, pipeline, dataops]
uses_functions:
  - profile_table_py_pipelines
  - infer_fk_containment_duckdb_py_datascience
  - build_join_graph_py_datascience
  - duckdb_list_tables_py_infra
  - render_eda_markdown_py_datascience
uses_types: []
returns: []
returns_optional: false
error_type: error_go_core
imports: []
tested: true
tests:
  - "profile_database_two_related_tables"
  - "profile_database_writes_report"
test_file_path: "python/functions/pipelines/profile_database_test.py"
file_path: "python/functions/pipelines/profile_database.py"
params:
  - name: db_path
    desc: "Ruta al archivo DuckDB (read-only, debe existir; no se crea)."
  - name: tables
    desc: "Lista de tablas a perfilar. None (default) usa todas las del esquema main via duckdb_list_tables."
  - name: sample
    desc: "Maximo de valores no nulos muestreados por columna en el perfil de cada tabla (se pasa a profile_table). Default 5000."
  - name: report_dir
    desc: "Directorio donde escribir los reports DB-level si write_report. Default 'reports'. Se crea si no existe."
  - name: write_report
    desc: "Si True (default) escribe report markdown DB-level + JSON sidecar timestamped en report_dir; si False no toca disco y los paths del retorno son None."
  - name: min_inclusion
    desc: "Umbral minimo de inclusion (0-1) para emitir una FK candidata (se pasa a infer_fk_containment_duckdb). Default 0.9."
output: "dict {status:'ok', db_profile:<DatabaseProfile con db_path, profiled_at, n_tables, tables[resumen], table_profiles[completos], fk_candidates, join_graph{nodes,edges,mermaid,hubs}, errors>, report_md_path:str|None, report_json_path:str|None} o {status:'error', error:str} (dict-no-throw)."
---

## Ejemplo

```python
import os
import tempfile
import duckdb
from pipelines.profile_database import profile_database

# Base DuckDB de juguete en /tmp: customers <- orders (relacionadas).
db = os.path.join(tempfile.mkdtemp(), "shop.duckdb")
con = duckdb.connect(db)
con.execute("CREATE TABLE customers (id INTEGER, name VARCHAR, city VARCHAR)")
con.execute("INSERT INTO customers VALUES (1,'Ana','Madrid'),(2,'Luis','Sevilla'),(3,'Marta','Bilbao')")
con.execute("CREATE TABLE orders (order_id INTEGER, customer_id INTEGER, total DOUBLE)")
con.execute("INSERT INTO orders VALUES (10,1,99.5),(11,1,12.0),(12,2,45.0),(13,3,7.25)")
con.close()

r = profile_database(db, write_report=False)
print(r["status"], r["db_profile"]["n_tables"])           # ok 2
print([fk["from_table"]+"."+fk["from_col"]+"->"+fk["to_table"]+"."+fk["to_col"]
       for fk in r["db_profile"]["fk_candidates"]])
# ['orders.customer_id->customers.id']  -> FK inferida por containment
print(r["db_profile"]["join_graph"]["mermaid"].splitlines()[0])  # graph LR

# Con report DB-level a disco (markdown con diagrama Mermaid + JSON sidecar):
r = profile_database(db, report_dir="reports")
print(r["report_md_path"], r["report_json_path"])
# reports/eda_db_20260620-101500.md reports/eda_db_20260620-101500.json
```

## Cuando usarla

Cuando necesites entender una BASE de datos entera de un golpe: el perfil de
todas sus tablas mas su esquema relacional (que tabla referencia a cual, con que
cardinalidad) en una sola llamada. Usala al recibir una base DuckDB desconocida,
para documentar un data warehouse, para descubrir el star schema (las tablas hub
del join graph) o antes de escribir joins sin tener el modelo declarado. Es el
escalon DB-level sobre `profile_table` (que perfila una sola tabla): aqui ademas
se infieren las FK y se dibuja el diagrama de relaciones.

## Gotchas

- Impura: con `write_report=True` (default) ESCRIBE dos archivos a `report_dir`
  (markdown DB-level + JSON sidecar). Pasa `write_report=False` para un dry-run
  sin tocar disco.
- Las FK se infieren por CONTAINMENT, es una HEURISTICA: A->B es candidata si los
  valores distintos de A estan contenidos en B (>= `min_inclusion`) y B parece
  clave (alta unicidad en su tabla). Puede dar falsos positivos (columnas que
  comparten dominio sin ser FK real, p.ej. dos columnas de codigos de pais) o
  perder FK reales si `min_inclusion` es muy alto o los datos estan sucios. Es un
  punto de partida para mapear el esquema, no un DDL autoritativo.
- Perfila TODAS las tablas por defecto: en bases grandes (muchas tablas o tablas
  muy anchas) puede TARDAR. Acota con `tables=[...]` o baja `sample`. La
  inferencia de FK ademas salta pares hacia tablas con mas de 200k filas (lado
  caro del INTERSECT); esas relaciones quedan sin evaluar.
- Tolera fallos por tabla: si el perfil de una tabla concreta falla, se anota en
  `db_profile["errors"]` y se sigue con las demas; `n_tables` cuenta solo las
  perfiladas con exito. Revisa `errors` para saber que quedo fuera.
- `db_path` debe existir: DuckDB read-only NO crea la base. El muestreo de cada
  tabla usa el sandbox read-only por defecto (sin acceso a FS/red).
