feat(duckdb,dav): primitivas de escritura DuckDB + libretas CardDAV + vCard multi-valor
Cinco funciones nuevas para soportar DuckDB como fuente de verdad del project osint:
Grupo duckdb (escritura, complementan a duckdb_query_readonly):
- duckdb_execute_py_infra (impure): ejecuta INSERT/UPDATE/DELETE/DDL en read-write, commit, {status,rowcount}. 6 tests.
- duckdb_upsert_py_infra (impure): UPSERT ON CONFLICT actualizando solo update_cols → ownership selectivo (un re-upsert no pisa columnas excluidas). 7 tests.
Grupo dav (libretas de contactos + vCard multi-valor):
- dav_make_addressbook_py_infra (impure): crea una libreta CardDAV nueva via extended MKCOL (RFC 5689). Idempotente. 12 tests.
- dav_list_addressbooks_py_infra (impure): lista las libretas del contacts-home (PROPFIND Depth:1). 7 tests.
- build_vcard_py_core (pure): serializa un contacto a vCard 3.0 multi-valor (N TEL/EMAIL/ADR + X-OSINT-*). 5 tests.
Paginas de capacidad duckdb.md y dav.md actualizadas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: duckdb_execute
|
||||
kind: function
|
||||
lang: py
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def duckdb_execute(db_path: str, sql: str, params: list = None) -> dict"
|
||||
description: "Ejecuta UNA sentencia de escritura (INSERT/UPDATE/DELETE/DDL) contra una base DuckDB abierta en conexion read-write (duckdb.connect(db_path)), hace commit y cierra siempre en try/finally. En modo escritura DuckDB crea el archivo si no existe. Es el primitivo de escritura del grupo duckdb; complementa a duckdb_query_readonly_py_infra (solo lectura). Usa parametros posicionales con el marcador '?'. Devuelve un dict sin lanzar (estilo del grupo): {status:'ok', rowcount} en exito y {status:'error', error} en fallo. rowcount es el numero de filas afectadas; DuckDB no expone un rowcount fiable (siempre -1) pero tras INSERT/UPDATE/DELETE el fetchall() del cursor devuelve [(n,)] de donde se extrae; para DDL u operaciones sin filas queda en -1 sin fallar. Depende del paquete duckdb (1.5.2 en python/.venv)."
|
||||
tags: [duckdb, sql, execute, write, ddl, dml]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_py_core"
|
||||
imports: [duckdb]
|
||||
params:
|
||||
- name: db_path
|
||||
desc: "ruta al archivo DuckDB. En modo escritura DuckDB crea el archivo si no existe. Un directorio padre inexistente o un lock de otro proceso devuelve {status:'error'}."
|
||||
- name: sql
|
||||
desc: "sentencia SQL de escritura (INSERT/UPDATE/DELETE/DDL). 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."
|
||||
output: "dict. En exito: {status:'ok', rowcount:int} donde rowcount es el numero de filas afectadas (o -1 cuando la sentencia no reporta filas, p.ej. DDL). En error (sin lanzar): {status:'error', error:str}."
|
||||
tested: true
|
||||
tests:
|
||||
- "test_insert_devuelve_status_ok_y_persiste"
|
||||
- "test_update_afecta_filas_y_persiste"
|
||||
- "test_delete_afecta_filas_y_persiste"
|
||||
- "test_ddl_create_table_status_ok"
|
||||
- "test_crea_la_base_si_no_existe"
|
||||
- "test_sql_invalido_devuelve_status_error"
|
||||
test_file_path: "python/functions/infra/duckdb_execute_test.py"
|
||||
file_path: "python/functions/infra/duckdb_execute.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys
|
||||
sys.path.insert(0, "python/functions")
|
||||
from infra.duckdb_execute import duckdb_execute
|
||||
|
||||
db = "/tmp/eventos.duckdb"
|
||||
|
||||
# DDL: crear la tabla (la base se crea sola si no existia).
|
||||
print(duckdb_execute(db, "CREATE TABLE eventos (id INTEGER, tipo VARCHAR)"))
|
||||
# {'status': 'ok', 'rowcount': -1} (DDL no reporta filas)
|
||||
|
||||
# DML: insertar con parametros posicionales.
|
||||
res = duckdb_execute(
|
||||
db,
|
||||
"INSERT INTO eventos VALUES (?, ?), (?, ?)",
|
||||
params=[1, "login", 2, "logout"],
|
||||
)
|
||||
print(res)
|
||||
# {'status': 'ok', 'rowcount': 2}
|
||||
|
||||
# UPDATE.
|
||||
print(duckdb_execute(db, "UPDATE eventos SET tipo = ? WHERE id = ?", params=["signin", 1]))
|
||||
# {'status': 'ok', 'rowcount': 1}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando un service single-writer necesita escribir DDL/DML en su DuckDB: crear o
|
||||
migrar tablas, insertar registros nuevos, actualizar estado o borrar filas en un
|
||||
archivo DuckDB que ese proceso posee. Es la mitad de escritura del grupo `duckdb`:
|
||||
usa `duckdb_query_readonly_py_infra` para leer (sin riesgo de modificar la base) y
|
||||
`duckdb_execute_py_infra` para escribir con commit. El dict de salida con
|
||||
`rowcount` es directamente serializable a JSON para pasarlo al siguiente paso de
|
||||
una composicion.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Escritura real de un archivo en disco (impura). Abre en modo read-write y hace
|
||||
commit; cualquier fallo se devuelve como `{status:'error', ...}`, nunca se lanza.
|
||||
- DuckDB es single-writer: solo un proceso puede tener la base abierta en
|
||||
escritura a la vez. Si otro proceso ya la tiene abierta en write, `connect`
|
||||
falla con un error de lock (`Could not set lock on file ...`) que se devuelve
|
||||
como `{status:'error', ...}`. Diseña el acceso para que un unico proceso sea el
|
||||
escritor; los lectores deben usar `duckdb_query_readonly` (read_only=True).
|
||||
- `rowcount` no es fiable en todos los casos. DuckDB no expone un `cursor.rowcount`
|
||||
util (siempre devuelve -1); esta funcion lee el conteo del `fetchall()` que
|
||||
DuckDB emite tras INSERT/UPDATE/DELETE (`[(n,)]`). Para DDL (`CREATE`/`DROP`/
|
||||
`ALTER`) y operaciones que no reportan filas, `rowcount` queda en `-1` a
|
||||
proposito: NO trates `-1` como error.
|
||||
- Ejecuta UNA sentencia por llamada (`con.execute(sql, params)`). No es para
|
||||
scripts multi-statement separados por `;`; para eso encadena varias llamadas o
|
||||
usa una funcion/pipeline dedicada.
|
||||
- Los parametros van en `params` con el marcador `?`, nunca interpolados en el
|
||||
string del SQL (previene inyeccion).
|
||||
- A diferencia del modo read-only, este modo **crea** el archivo si no existe. Un
|
||||
`db_path` con un directorio padre inexistente si falla y se reporta como error.
|
||||
Reference in New Issue
Block a user