---
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: <valor>', 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: <valor>', 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.
