--- name: load_folder_to_duckdb kind: function lang: py domain: infra version: "1.0.0" purity: impure signature: "def load_folder_to_duckdb(folder: str, db_path: str = None, pattern: str = '*.csv,*.parquet,*.json') -> dict" description: "Escanea el primer nivel de una CARPETA buscando archivos tabulares (CSV/TSV/TXT, Parquet, JSON/NDJSON) y los carga como tablas en una base DuckDB usando los lectores nativos read_csv_auto/read_parquet/read_json_auto. Es la pieza de entrada del EDA a nivel de carpeta (grupo eda). Por cada archivo crea una tabla cuyo nombre se deriva del basename saneado a [0-9a-zA-Z_] en minusculas (prefijo t_ si empieza por digito, sufijos _2/_3 ante colisiones, tabla_ si queda vacio). El path se escapa (comilla simple '->'') antes de interpolarlo porque los lectores DuckDB no aceptan el path como parametro posicional. Glob NO recursivo: un glob.glob(os.path.join(folder, g)) por cada patron del CSV, dedup y ordenado. db_path=None genera una DuckDB temporal (mkstemp, se borra el placeholder vacio porque DuckDB rechaza un archivo de 0 bytes) y devuelve su ruta. Un fallo al cargar un archivo concreto no aborta el resto: se registra en errors y se continua. Devuelve siempre un dict sin lanzar (estilo del grupo duckdb): {status:'ok', db_path, tables, errors} en exito (carpeta sin archivos tabulares incluida, tables=[]) y {status:'error', error} cuando la carpeta no existe o falla algo global. Depende del paquete duckdb (1.5.2)." tags: [eda, duckdb, ingest, etl, folder] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_py_core" imports: [glob, os, re, tempfile, duckdb] params: - name: folder desc: "ruta a un directorio. Se escanea solo su primer nivel (NO recursivo). Si no existe o no es un directorio devuelve {status:'error'} sin lanzar." - name: db_path desc: "ruta del archivo DuckDB destino, abierto en modo read-write (lo crea si no existe). None (default) genera una DuckDB temporal unica con tempfile.mkstemp y devuelve su ruta en el campo db_path del retorno. DuckDB es single-writer: si otro proceso lo tiene abierto en escritura, connect falla con error de lock devuelto en el dict." - name: pattern desc: "CSV de globs separados por coma (default '*.csv,*.parquet,*.json'). Cada glob se aplica con glob.glob(os.path.join(folder, g)) sobre el primer nivel de folder; los resultados de todos los globs se deduplican y ordenan. Los globs con ** NO descienden recursivamente (glob.glob sin recursive=True)." output: "dict. En exito: {status:'ok', db_path:str (ruta DuckDB usada), tables:[{name:str, source_file:str, n_rows:int}], errors:[{name?:str, source_file:str, error:str}]}. La carpeta sin archivos tabulares es un exito con tables=[] y errors=[]. En error (sin lanzar): {status:'error', error:str}." tested: true tests: - "test_carga_dos_csv_como_tablas" - "test_db_path_none_crea_temporal" - "test_carpeta_vacia_es_ok_sin_tablas" - "test_carpeta_inexistente_devuelve_status_error" test_file_path: "python/functions/infra/load_folder_to_duckdb_test.py" file_path: "python/functions/infra/load_folder_to_duckdb.py" --- ## Ejemplo ```python import sys sys.path.insert(0, "python/functions") from infra.load_folder_to_duckdb import load_folder_to_duckdb # Preparar una carpeta de demo con dos CSV. import os os.makedirs("/tmp/eda_folder_demo", exist_ok=True) with open("/tmp/eda_folder_demo/ventas.csv", "w") as f: f.write("id,total\n1,10.5\n2,20.0\n3,5.25\n") with open("/tmp/eda_folder_demo/clientes.csv", "w") as f: f.write("id,nombre\n1,ana\n2,luis\n") # Cargar todos los tabulares de la carpeta a una DuckDB temporal. res = load_folder_to_duckdb("/tmp/eda_folder_demo") print(res["status"]) # ok print(res["db_path"]) # /tmp/tmpXXXXXXXX.duckdb (temporal) for t in res["tables"]: print(t["name"], t["n_rows"]) # ventas 3 / clientes 2 # Persistir en una DuckDB concreta y limitar a CSV. res2 = load_folder_to_duckdb( "/tmp/eda_folder_demo", db_path="/tmp/eda_folder_demo/folder.duckdb", pattern="*.csv", ) print(res2["tables"]) # [{'name': 'clientes', ...}, {'name': 'ventas', ...}] ``` ## Cuando usarla Cuando tienes una carpeta de datos sueltos (un dump, un export, varios CSV/Parquet descargados) y quieres analizarlos juntos con SQL sin montar la ingesta a mano, archivo por archivo. Es el primer eslabon del EDA a nivel de carpeta (grupo `eda`): deja una DuckDB con una tabla por archivo, lista para perfilar con `duckdb_table_schema_py_infra`, consultar con `duckdb_query_readonly_py_infra`, o correlacionar aguas abajo. Usala antes de cualquier paso de perfilado cuando la unidad de trabajo es "todos los archivos de este directorio". ## Gotchas - **Glob NO recursivo**: solo se escanea el primer nivel de `folder`. Archivos en subdirectorios se ignoran (ni siquiera con `**` en el patron, porque `glob.glob` se llama sin `recursive=True`). Si necesitas recursion, aplana la carpeta antes o amplia la funcion. - **Saneo de nombres de tabla**: el basename se reduce a `[0-9a-zA-Z_]` en minusculas. `Ventas 2024.csv` -> tabla `ventas_2024`. Dos archivos distintos pueden sanear al mismo nombre (`a-b.csv` y `a_b.csv`); el segundo se desambigua con sufijo `_2`, `_3`, ... El mapeo real archivo->tabla esta en `tables[].name` / `tables[].source_file`, no lo asumas. - **`read_json_auto` requiere JSON tabular** (array de objetos u objetos NDJSON homogeneos). Un JSON anidado o irregular puede fallar la carga de ESA tabla; el error se registra en `errors` y el resto de archivos siguen cargandose. - **Extension desconocida = se salta**, no falla: queda anotada en `errors` con `unsupported extension`. Mapeo de lectores: `.csv/.tsv/.txt`->`read_csv_auto`, `.parquet/.pq`->`read_parquet`, `.json/.ndjson`->`read_json_auto`. - **Escritura real en disco (impura)**. DuckDB es single-writer: si otro proceso tiene `db_path` abierto en escritura, `connect` falla con error de lock devuelto en el dict. Un `db_path` con un directorio padre inexistente tambien falla. - **`db_path=None` crea un archivo temporal que NO se borra solo**: la ruta se devuelve en `db_path` para que el llamador la consuma y la limpie cuando termine. - **Tipos inferidos por los lectores `_auto`**: los tipos de columna los infiere DuckDB. Revisa el schema con `duckdb_table_schema_py_infra` si el tipado importa aguas abajo.