fix(eda): bugs de bajo riesgo del benchmark (H1,H5,H12,H13,H14) + tests faltantes

- H1: render_eda_markdown ya no aplica doble x100 a outlier_pct (336% -> real)
- H5: profile_database filtra base_tables_only (excluye VIEWs; sakila 21->16)
- H12: suggest_reexpression salta columnas no-continuas
- H13: to_returns/profile_table elige retornos (financiera) vs diferencias (fisica)
- H14: test de regresion ATTACH sqlite via information_schema
- +8 tests de las funciones eda nuevas (acf_pacf, adf_kpss, ...). 77 tests verdes
- L/M (H2,H3,H4,H6,H7,H8,H9,H10,H11) quedan en issues 0174-0177 para revision

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Egutierrez
2026-06-29 03:51:11 +02:00
parent 7ac69ab4fb
commit caf8c25d99
17 changed files with 1145 additions and 31 deletions
+9 -4
View File
@@ -5,8 +5,8 @@ lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def duckdb_list_tables(db_path: str) -> dict"
description: "Lista las tablas de una base DuckDB abierta en modo solo lectura (duckdb.connect(db_path, read_only=True)), de modo que nunca crea ni modifica la base. La conexion se cierra siempre en try/finally. Consulta information_schema.tables del esquema main y devuelve los nombres ordenados alfabeticamente. Devuelve un dict sin lanzar (estilo del grupo duckdb): {status:'ok', tables} en exito y {status:'error', error} en fallo. Es la introspeccion 'que tablas hay' del grupo duckdb; complementa a duckdb_query_readonly_py_infra (lectura de filas) y a duckdb_table_schema_py_infra (schema de una tabla). Depende del paquete duckdb (1.5.2 en python/.venv)."
signature: "def duckdb_list_tables(db_path: str, base_tables_only: bool = False) -> dict"
description: "Lista las tablas de una base DuckDB abierta en modo solo lectura (duckdb.connect(db_path, read_only=True)), de modo que nunca crea ni modifica la base. La conexion se cierra siempre en try/finally. Consulta information_schema.tables del esquema main y devuelve los nombres ordenados alfabeticamente. Con base_tables_only=True filtra table_type='BASE TABLE', excluyendo las VIEWs (util para perfilar/relacionar solo tablas reales). Devuelve un dict sin lanzar (estilo del grupo duckdb): {status:'ok', tables} en exito y {status:'error', error} en fallo. Es la introspeccion 'que tablas hay' del grupo duckdb; complementa a duckdb_query_readonly_py_infra (lectura de filas) y a duckdb_table_schema_py_infra (schema de una tabla). Depende del paquete duckdb (1.5.2 en python/.venv)."
tags: [duckdb, sql, introspection, readonly, tables]
uses_functions: []
uses_types: []
@@ -17,12 +17,16 @@ imports: [duckdb]
params:
- name: db_path
desc: "ruta al archivo DuckDB. Debe existir: el modo read_only NO crea la base. Un path inexistente devuelve {status:'error'}."
- name: base_tables_only
desc: "si True (default False) filtra table_type='BASE TABLE', excluyendo las VIEWs del esquema main. Util para perfilar/relacionar solo tablas reales (perfilar una VIEW infla el conteo y multiplica relaciones FK falsas)."
output: "dict. En exito: {status:'ok', tables:[str,...]} con los nombres de tabla del esquema main ordenados alfabeticamente. En error (sin lanzar): {status:'error', error:str}."
tested: true
tests:
- "test_lista_tablas_ordenadas"
- "test_base_vacia_devuelve_lista_vacia"
- "test_db_inexistente_devuelve_status_error"
- "test_base_tables_only_excluye_views"
- "test_attach_sqlite_materializado_lista_por_information_schema"
test_file_path: "python/functions/infra/duckdb_list_tables_test.py"
file_path: "python/functions/infra/duckdb_list_tables.py"
---
@@ -64,7 +68,8 @@ selector de tablas en una UI. Es el primer paso natural antes de
- DuckDB es single-writer: si otro proceso tiene la base abierta en escritura con
una version distinta del motor, la apertura read-only puede fallar con error de
lock. El error se devuelve como `{status:'error', ...}`, no se lanza.
- Solo lista tablas del esquema `main` (el por defecto). Vistas y tablas de otros
esquemas no aparecen.
- Solo lista objetos del esquema `main` (el por defecto); tablas de otros esquemas
no aparecen. Por defecto incluye **vistas** (table_type VIEW) además de las tablas
base; pasa `base_tables_only=True` para quedarte solo con las `BASE TABLE`.
- Una base recien creada sin tablas devuelve `{status:'ok', tables:[]}` (no es un
error): lista vacia.
+15 -4
View File
@@ -13,12 +13,19 @@ introspeccion de alto nivel "que tablas hay" del grupo duckdb.
"""
def duckdb_list_tables(db_path: str) -> dict:
def duckdb_list_tables(db_path: str, base_tables_only: bool = False) -> dict:
"""Lista las tablas de una base DuckDB en modo solo lectura.
Args:
db_path: ruta al archivo DuckDB. Debe existir: el modo read_only NO crea
la base. Un path inexistente devuelve {status:'error', ...}.
base_tables_only: si True (default False) filtra por
`table_type = 'BASE TABLE'`, excluyendo las VIEWs (y demas objetos no
tabla-base) del esquema `main`. Util para perfilar/relacionar solo las
tablas reales: perfilar una VIEW infla el numero de tablas y multiplica
las relaciones FK falsas. El default mantiene el comportamiento previo
(lista todo lo que aparece en information_schema.tables del esquema
main) para no romper consumidores existentes.
Returns:
dict. En exito: {status:'ok', tables:[str,...]} con los nombres de tabla
@@ -28,10 +35,14 @@ def duckdb_list_tables(db_path: str) -> dict:
conn = None
try:
conn = __import__("duckdb").connect(db_path, read_only=True)
rows = conn.execute(
sql = (
"SELECT table_name FROM information_schema.tables "
"WHERE table_schema = 'main' ORDER BY table_name"
).fetchall()
"WHERE table_schema = 'main'"
)
if base_tables_only:
sql += " AND table_type = 'BASE TABLE'"
sql += " ORDER BY table_name"
rows = conn.execute(sql).fetchall()
tables = [row[0] for row in rows]
return {"status": "ok", "tables": tables}
except Exception as e: # noqa: BLE001
@@ -38,3 +38,59 @@ def test_db_inexistente_devuelve_status_error(tmp_path):
res = duckdb_list_tables(str(tmp_path / "noexiste.duckdb"))
assert res["status"] == "error"
assert "error" in res
def test_base_tables_only_excluye_views(tmp_path):
# Una BASE TABLE + una VIEW: por defecto se listan ambas; con
# base_tables_only=True la VIEW se excluye.
db = tmp_path / "withviews.duckdb"
con = duckdb.connect(str(db))
con.execute("CREATE TABLE ventas (id INTEGER, total DOUBLE)")
con.execute("CREATE VIEW ventas_resumen AS SELECT id FROM ventas")
con.close()
# Default: incluye la view.
res_all = duckdb_list_tables(str(db))
assert res_all["status"] == "ok"
assert res_all["tables"] == ["ventas", "ventas_resumen"]
# base_tables_only: solo la tabla base.
res_base = duckdb_list_tables(str(db), base_tables_only=True)
assert res_base["status"] == "ok"
assert res_base["tables"] == ["ventas"]
def test_attach_sqlite_materializado_lista_por_information_schema(tmp_path):
# Regresión H14: tras ATTACH de una base SQLite en DuckDB se materializan sus
# tablas y se listan vía information_schema (NO sqlite_master, que no existe en
# DuckDB). duckdb_list_tables debe verlas como tablas del esquema main.
import sqlite3
sqlite_path = str(tmp_path / "src.sqlite")
sconn = sqlite3.connect(sqlite_path)
sconn.execute("CREATE TABLE clientes (id INTEGER PRIMARY KEY, nombre TEXT)")
sconn.execute("INSERT INTO clientes VALUES (1,'Ana'),(2,'Luis')")
sconn.execute("CREATE VIEW clientes_v AS SELECT id FROM clientes")
sconn.commit()
sconn.close()
ddb_path = str(tmp_path / "materialized.duckdb")
con = duckdb.connect(ddb_path)
con.execute("INSTALL sqlite")
con.execute("LOAD sqlite")
con.execute(f"ATTACH '{sqlite_path}' AS src (TYPE sqlite)")
# Listar tablas base del catálogo attachado por information_schema (no
# sqlite_master) y materializarlas como tablas nativas DuckDB.
rows = con.execute(
"SELECT table_name FROM information_schema.tables "
"WHERE table_catalog='src' AND table_type='BASE TABLE' "
"AND table_name NOT LIKE 'sqlite_%'"
).fetchall()
for (name,) in rows:
con.execute(f'CREATE TABLE "{name}" AS SELECT * FROM src."{name}"')
con.execute("DETACH src")
con.close()
res = duckdb_list_tables(ddb_path)
assert res["status"] == "ok"
assert "clientes" in res["tables"]