--- name: extract_text_sample kind: function lang: py domain: datascience version: "1.0.0" purity: impure signature: "def extract_text_sample(db_path: str, table: str, columns: list, backend: str = 'duckdb', sample: int = 2000) -> dict" description: "Muestrea columnas de texto de una tabla DuckDB/Postgres con push-down SQL (LIMIT sample), SIN traer la tabla entera a RAM. Funcion impura del grupo de capacidad `eda`: la usan los capitulos de texto/NLP del AutomaticEDA que necesitan valores crudos de texto (longitudes, tokens, ejemplos) sobre una muestra acotada. Construye el lector read-only query_fn(sql)->dict igual que build_eda_render_ctx (closure sobre duckdb_query_readonly / pg_query importados perezosamente desde infra). Escapa los identificadores con comillas dobles y lanza una sola query SELECT \"c1\", \"c2\" FROM \"table\" LIMIT n. Por columna, la lista de strings solo contiene valores NO None y NO vacios: cada celda no nula se convierte con str(...) y se descarta si queda cadena vacia. Estilo dict-no-throw del grupo eda: NUNCA lanza; ante cualquier fallo (query, conversion, backend desconocido) devuelve {status:'error', error:str, columns:{}, n:0}. La clave n reporta el numero de FILAS leidas por la query (antes de filtrar None/vacios)." tags: [eda, datascience, text, nlp, extraction, read-only, duckdb, postgres, python] uses_functions: [duckdb_query_readonly_py_infra, pg_query_py_infra] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [] params: - name: db_path desc: "ruta al archivo DuckDB, o DSN PostgreSQL si backend='postgres'. Se inyecta en el closure query_fn. No se valida aqui: si la base no existe o el DSN es invalido, la query devuelve status error y el resultado es {status:'error', ...} (no lanza)." - name: table desc: "nombre de la tabla. Se escapa con comillas dobles en la query (SELECT ... FROM \"table\")." - name: columns desc: "lista de nombres de columna de texto a muestrear. Se filtra a las entradas que sean str no vacio; cada nombre se escapa con comillas dobles. Si tras filtrar queda vacia -> {status:'ok', columns:{}, n:0} sin tocar la base." - name: backend desc: "'duckdb' (default) o 'postgres'. Selecciona el lector read-only del registry (duckdb_query_readonly / pg_query). Cualquier otro valor -> {status:'error', error:'backend desconocido: ', columns:{}, n:0}." - name: sample desc: "maximo de filas a muestrear (clausula LIMIT). Default 2000. Acota memoria y tiempo: con tablas grandes obtienes el primer tramo por orden fisico (sin ORDER BY), no un muestreo uniforme." output: "dict dict-no-throw (NUNCA lanza): {status:'ok'|'error', columns:{col_name:[str,...]}, n:int, error:str}. En exito (status='ok') columns mapea cada columna pedida a la lista de sus valores de texto NO None y NO vacios (cada celda convertida con str(...)); n es el numero de FILAS leidas por la query (antes de filtrar None/vacios). columns vacio -> {status:'ok', columns:{}, n:0}. En error (backend desconocido, query con status!='ok', o cualquier excepcion) -> {status:'error', error:str, columns:{}, n:0}; la clave error solo aparece en este caso." tested: true tests: ["test_extract_basic", "test_backend_desconocido", "test_columns_vacio", "test_sample_limit"] test_file_path: "python/functions/datascience/extract_text_sample_test.py" file_path: "python/functions/datascience/extract_text_sample.py" --- ## Ejemplo ```python import sys, os sys.path.insert(0, os.path.join("python", "functions")) # Import directo del submodulo (no requiere export en datascience/__init__.py). from datascience.extract_text_sample import extract_text_sample # Muestrea hasta 2000 filas de dos columnas de texto de una tabla DuckDB. res = extract_text_sample( "data/reviews.duckdb", "reviews", ["title", "body"], backend="duckdb", sample=2000, ) # res == { # "status": "ok", # "columns": { # "title": ["Gran producto", "No funciona", ...], # solo no-None, no-"" # "body": ["Lo uso a diario...", ...], # }, # "n": 2000, # filas leidas por la query (antes de filtrar None/vacios) # } # Postgres: db_path es el DSN. res_pg = extract_text_sample( "postgresql://user:pass@localhost:5433/trends", "comentarios", ["texto"], backend="postgres", sample=500, ) ``` ## Cuando usarla Cuando necesites valores CRUDOS de texto de una o varias columnas para analisis NLP/texto (distribucion de longitudes, conteo de tokens, ejemplos representativos, deteccion de idioma) pero NO quieras cargar la tabla entera en memoria. Es el muestreador de texto del grupo `eda`: una sola llamada con push-down `LIMIT` devuelve listas de strings por columna, limpias de None y vacios, listas para alimentar un capitulo de texto del AutomaticEDA o cualquier rutina de tokenizado. Usala junto a `profile_table` / `build_eda_render_ctx` cuando el perfil agregado no basta y hace falta el texto real. ## Gotchas - **Impura**: lee de la base de datos a traves de `query_fn` (closure sobre `duckdb_query_readonly` / `pg_query`). No abre conexiones fuera de esos wrappers del registry. Estilo dict-no-throw del grupo `eda`: NUNCA lanza; ante cualquier fallo devuelve `{status:'error', error:str, columns:{}, n:0}`. - **`error_type` en el frontmatter es `error_go_core` por convencion del registry** (toda funcion impura debe declararlo y el indexer lo exige), pero el codigo NO lanza esa excepcion: degrada al dict de error. Es metadata, no comportamiento. - **Backend desconocido**: con un `backend` que no sea `duckdb` ni `postgres` devuelve `{status:'error', error:'backend desconocido: ', columns:{}, n:0}` sin tocar la base. - **Las listas NO incluyen None ni cadenas vacias**: cada celda no nula se pasa por `str(...)` y se descarta si queda `""`. Por eso `len(columns[col])` puede ser menor que `n` (que cuenta las filas leidas). Si necesitas alineacion por fila (una entrada por fila aunque sea None), usa `build_eda_render_ctx` (raw_numeric), no esta funcion. - **`LIMIT sample` sin `ORDER BY`**: con tablas grandes obtienes el primer tramo por orden fisico del backend, no un muestreo uniforme ni reproducible. Sube `sample` para mas cobertura, o pre-ordena/aleatoriza la tabla si necesitas representatividad. - **DuckDB en sandbox por defecto**: `duckdb_query_readonly` abre la conexion con `enable_external_access=False`, asi que la query solo puede leer la propia base (no `read_csv`/`httpfs`/`ATTACH` a paths externos). Lee tablas ya existentes en el archivo DuckDB sin problema. - **No loguear los datos crudos**: las listas de `columns` pueden contener texto sensible (reviews, comentarios, PII). En trazas usa solo conteos (`n`, `len(columns[col])`) y nombres de columna, no el dict completo.