chore: auto-commit (9 archivos)
- docs/capabilities/INDEX.md - docs/capabilities/obsidian.md - python/functions/core/render_markdown_table.md - python/functions/core/render_markdown_table.py - python/functions/core/render_markdown_table_test.py - python/functions/core/upsert_sentinel_block.md - python/functions/core/upsert_sentinel_block.py - python/functions/core/upsert_sentinel_block_test.py - python/functions/infra/duckdb_query_readonly.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user