--- id: "0175" title: "EDA relational: precisión de FK inference (falsos positivos) + filtrar VIEWs + test ATTACH" status: pendiente type: bugfix domain: - registry-quality scope: registry-only priority: alta depends: [] blocks: [] related: ["0173", "0174", "0176", "0177"] created: 2026-06-29 updated: 2026-06-29 tags: [eda, datascience, infer_fk_containment_duckdb, build_join_graph, profile_database, duckdb, benchmark] --- # 0175 — EDA relational: precisión de FK inference + filtrar VIEWs ## Contexto El benchmark `/eda` (29/06/2026, `temp/eda_benchmark/EVALUATION.md`) confirmó que la inferencia de claves foráneas a nivel de base es **inútil por falsos positivos masivos** y que las VISTAS se perfilan como tablas base. El join graph resultante necesita filtrado manual para ser legible. Hallazgos cubiertos: | Hallazgo | Severidad | Evidencia del benchmark | |---|---|---| | H3 — FK inference por contención: 10-20× falsos positivos | crítico | chinook 111 candidatas vs ~11 reales; sakila 565 vs ~30. Casos absurdos: `InvoiceLine.Quantity→Album.AlbumId`, `Genre.GenreId→{Album,Artist,Customer,…}` | | H5 — VIEWs perfiladas como tablas base | alto | sakila `n_tables=21` incluye 5 VISTAS (`customer_list`, `film_list` 5462 filas, `staff_list`, `sales_by_store`, `sales_by_film_category`) + `film_text` (FTS, 0 filas) | | H10 — coste relacional gastado en computar FK falsas | medio | sakila 31.82s: la mayoría en INTERSECT de los 565 pares candidatos, casi todos falsos | | H14 — bug `sqlite_master does not exist` tras ATTACH (ya parcheado, falta test) | bajo (resuelto) | `_run.log`: `profile_database` falló con `Catalog Error: src.sqlite_master`; re-run posterior `ok` | ### Causa raíz (verificada en código, READ-ONLY) - `python/functions/datascience/infer_fk_containment_duckdb.py:217-285` emite una FK candidata si `inclusion(A⊆B) ≥ min_inclusion` **y** B "parece clave" (unicidad ≥0.95). **No usa el nombre de la columna**, que es la señal más fuerte de FK (`AlbumId→Album.AlbumId`), ni excluye columnas no-clave (cantidades, importes) como ORIGEN. Enteros pequeños (`GenreId` 1..25) están contenidos en casi todo → ruido. - `python/functions/pipelines/profile_database.py:155-159` lista tablas con `duckdb_list_tables` sin filtrar `table_type` → perfila VIEWs y tablas FTS como base (H5), lo que infla el universo de pares y multiplica las FK falsas (relaciona H10). - H10 es el **mismo cambio** que H3: filtrar candidatos por nombre **antes** del INTERSECT reduce pares (más rápido) y falsos positivos (más preciso) a la vez. ## Tareas 1. **H3+H10 — señal de nombre en `infer_fk_containment_duckdb.py:217-285`:** antes de lanzar el INTERSECT, exigir coincidencia/patrón de nombre entre origen y destino (`from_col` casa con `to_table`/`to_col`, patrón `Id → .Id`; case-insensitive). Excluir como ORIGEN columnas claramente no-clave (cantidades, importes, flags) por heurística de nombre/tipo. Esto poda el O(tablas²×columnas²) y elimina la mayoría de los falsos positivos. Validar mejor la cardinalidad (los `1:1` imposibles del benchmark). 2. **H5 — filtrar VIEWs** antes de perfilar e inferir FK: filtrar `table_type='BASE TABLE'` vía `information_schema.tables` / `duckdb_tables()`. Decidir (a confirmar al implementar) si el filtro va como flag nuevo en `duckdb_list_tables` (infra, reutilizable) o en `profile_database.py` tras listar. Preferir el flag en `duckdb_list_tables` si no rompe consumidores. 3. **H3 — propagar al join graph:** verificar que `build_join_graph.py` recibe la lista ya filtrada y que el diagrama Mermaid resultante es legible (sin nodos VIEW ni aristas espurias). 4. **H14 — test de regresión:** añadir test (en `profile_database_test.py` o `infer_fk_containment_duckdb_test.py`) que haga `ATTACH` de una base SQLite pequeña en DuckDB y perfile, confirmando que se usa `information_schema`/`duckdb_tables()` y nunca `sqlite_master`. (A confirmar: localizar la función que hace el ATTACH —probablemente `summarize_table_duckdb.py` o una primitiva infra `duckdb_*`— para cubrirla.) 5. Tests: casos sintéticos con tablas que tengan columnas tipo `XId` (FK real) y columnas de cantidad contenidas en claves (falso positivo) → confirmar que solo emite las reales. ## Definition of Done | Escenario | Tipo | Comando / evidencia | Resultado esperado | |---|---|---|---| | Golden: FK reales sin ruido | e2e | re-correr `profile_database` sobre chinook | ~11 FK candidatas (no 111); incluyen `Album.ArtistId→Artist.ArtistId`, `Invoice.CustomerId→Customer.CustomerId`; NO incluyen `InvoiceLine.Quantity→Album.AlbumId` | | Edge: VIEWs excluidas | e2e | re-correr `profile_database` sobre sakila | `n_tables` cuenta solo BASE TABLE (sin `customer_list`/`film_list`/…); FK candidatas ≪ 565 | | Edge: cantidad vs clave | unit | `infer_fk_containment_duckdb_test.py` con columna `Quantity` contenida en una clave | NO emite FK desde `Quantity` | | Error: ATTACH SQLite | unit | test de regresión ATTACH SQLite→DuckDB | perfila sin `sqlite_master does not exist`; usa information_schema | | Rendimiento (H10) | e2e | medir duración de `profile_database` sobre sakila | menor que el baseline 31.82s (menos INTERSECT) | | Mecánica | — | `./fn run infer_fk_containment_duckdb_py_datascience`, `./fn run profile_database_py_pipelines`; `fn index` | tests verdes; índice limpio | Re-correr el benchmark sobre chinook y sakila y confirmar que las FK reales son distinguibles del ruido y que las VIEWs no se cuentan como tablas. ## Notas Issue derivado de `temp/eda_benchmark/EDA_ISSUES.md`. Tres síntomas (H3/H5/H10) con un núcleo común: la capa de inferencia de relaciones inter-tabla. Atacarlos juntos en una rama; filtrar VIEWs reduce el universo de pares y filtrar candidatos por nombre arregla precisión y velocidad a la vez. H14 ya está parcheado en producción; este issue solo añade el test de regresión que faltaba. Hermanos: 0173, 0174, 0176, 0177.