--- name: duckdb_query_readonly kind: function lang: py domain: infra version: "1.0.0" purity: impure signature: "def duckdb_query_readonly(db_path: str, sql: str, params: list = None, max_rows: int = 10000) -> dict" description: "Ejecuta una query SELECT contra 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. Usa parametros posicionales con el marcador '?'. Devuelve un dict sin lanzar (estilo del grupo dav): {status:'ok', columns, rows, row_count, truncated} en exito y {status:'error', error} en fallo. Las filas son list[dict]. Trunca a max_rows para proteger memoria. Convierte valores no serializables: date/datetime/time a isoformat(), Decimal a float, bytes a base64, UUID a str. Depende del paquete duckdb (1.5.2 en python/.venv)." tags: [duckdb, sql, query, readonly] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_py_core" imports: [base64, datetime, decimal, uuid, 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: sql desc: "sentencia SQL a ejecutar (pensada para SELECT). Usa el marcador '?' para parametros posicionales." - name: params desc: "lista de parametros posicionales para el SQL en orden. None (default) significa sin parametros. Pasar valores aqui en vez de interpolarlos en el SQL evita inyeccion." - name: max_rows desc: "numero maximo de filas a materializar en memoria. Default 10000. Si la query produce mas, el resultado se trunca y truncated queda en True." output: "dict. En exito: {status:'ok', columns:[str,...], rows:[{col:val,...},...], row_count:int, truncated:bool}. En error (sin lanzar): {status:'error', error:str}. Los valores de las filas estan normalizados a tipos JSON-serializables." tested: true tests: - "test_query_ok_devuelve_filas_como_dicts" - "test_query_con_params_posicionales" - "test_sql_invalido_devuelve_status_error" - "test_db_inexistente_devuelve_status_error" - "test_truncado_a_max_rows" - "test_valores_no_serializables_se_convierten" test_file_path: "python/functions/infra/duckdb_query_readonly_test.py" file_path: "python/functions/infra/duckdb_query_readonly.py" --- ## Ejemplo ```python import sys sys.path.insert(0, "python/functions") import duckdb from infra.duckdb_query_readonly import duckdb_query_readonly # Preparamos una base de ejemplo (esto seria un proceso separado en la realidad). db = "/tmp/ventas.duckdb" con = duckdb.connect(db) con.execute("CREATE TABLE ventas (id INTEGER, region VARCHAR, total DECIMAL(10,2))") con.execute("INSERT INTO ventas VALUES (1, 'norte', 120.50), (2, 'sur', 80.00), (3, 'norte', 45.25)") con.close() # Lectura solo-lectura con parametro posicional. res = duckdb_query_readonly( db, "SELECT region, SUM(total) AS total FROM ventas WHERE region = ? GROUP BY region", params=["norte"], ) print(res["status"]) # ok print(res["columns"]) # ['region', 'total'] print(res["rows"]) # [{'region': 'norte', 'total': 165.75}] print(res["truncated"]) # False ``` ## Cuando usarla Cuando necesitas leer datos de un archivo DuckDB sin riesgo de modificarlo: inspeccionar una base materializada, validar el resultado de un pipeline, alimentar un dashboard o un report, o consultar tablas/Parquet exportados por otra funcion del registry. El modo read_only garantiza que la consulta nunca crea ni altera la base, y el dict de salida es directamente serializable a JSON para pasarlo al siguiente paso de una composicion. ## Gotchas - Lectura real de un archivo en disco (impura). El modo `read_only=True` exige que el archivo **ya exista**: a diferencia del modo escritura, no crea la base. Si `db_path` no existe, devuelve `{status:'error', error:...}`. - Conflicto de lock: si otro proceso tiene la misma base abierta en escritura con una version de DuckDB distinta, la apertura puede fallar (DuckDB no permite abrir un archivo bloqueado por otra version del motor). El error se devuelve como `{status:'error', ...}`, no se lanza. - `max_rows` protege la memoria: una query que devuelve millones de filas se trunca a `max_rows` y marca `truncated=True`. Si necesitas todas las filas, pagina con LIMIT/OFFSET en el SQL o sube `max_rows` conscientemente. - Los parametros van en `params` con el marcador `?`, nunca interpolados en el string del SQL (previene inyeccion). - Valores no JSON-serializables se normalizan en la salida: date/datetime/time a `isoformat()`, Decimal a float (puede haber perdida de precision frente al decimal exacto), bytes a base64 y UUID a str.