1c4a4b9259
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>
83 lines
3.3 KiB
Python
83 lines
3.3 KiB
Python
"""Ejecuta una sentencia de escritura (INSERT/UPDATE/DELETE/DDL) contra DuckDB.
|
|
|
|
Funcion impura: abre un archivo DuckDB con `duckdb.connect(db_path)` en modo
|
|
read-write (crea el archivo si no existe, cosa que el modo escritura de DuckDB
|
|
permite). Ejecuta UNA sentencia con parametros posicionales (DuckDB usa el
|
|
marcador `?`), hace commit y cierra la conexion siempre en un bloque try/finally.
|
|
|
|
Es el primitivo de escritura del grupo `duckdb` del registry; complementa a
|
|
`duckdb_query_readonly_py_infra`, que es solo lectura.
|
|
|
|
Devuelve un dict sin lanzar excepciones, siguiendo el estilo del grupo
|
|
(`{status:'ok', ...}` en exito, `{status:'error', error:str}` en fallo). En exito
|
|
incluye `rowcount`: el numero de filas afectadas por la sentencia. DuckDB no expone
|
|
un `rowcount` fiable en su cursor (siempre devuelve -1), pero tras un
|
|
INSERT/UPDATE/DELETE el `fetchall()` del cursor devuelve `[(n,)]` con el conteo;
|
|
de ahi se extrae. Para DDL u operaciones que no reportan filas, `rowcount` queda
|
|
en -1 y eso NUNCA hace fallar la funcion.
|
|
"""
|
|
|
|
|
|
def _affected_rowcount(cursor) -> int:
|
|
"""Extrae el numero de filas afectadas de un cursor DuckDB de escritura.
|
|
|
|
Estrategia robusta para DuckDB:
|
|
1. Si `cursor.rowcount` esta disponible y es >= 0, usarlo.
|
|
2. Si no, intentar `cursor.fetchall()`: tras INSERT/UPDATE/DELETE DuckDB
|
|
devuelve `[(n,)]` con el conteo. Se extrae el primer entero.
|
|
3. Si nada aplica (DDL, sin filas), devolver -1.
|
|
|
|
Nunca lanza: cualquier problema al leer el conteo cae a -1.
|
|
"""
|
|
try:
|
|
rc = getattr(cursor, "rowcount", -1)
|
|
if isinstance(rc, int) and rc >= 0:
|
|
return rc
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
|
|
try:
|
|
fetched = cursor.fetchall()
|
|
except Exception: # noqa: BLE001
|
|
return -1
|
|
|
|
if fetched and fetched[0]:
|
|
candidate = fetched[0][0]
|
|
if isinstance(candidate, int):
|
|
return candidate
|
|
|
|
return -1
|
|
|
|
|
|
def duckdb_execute(db_path: str, sql: str, params: list = None) -> dict:
|
|
"""Ejecuta una sentencia de escritura DuckDB en conexion read-write.
|
|
|
|
Args:
|
|
db_path: ruta al archivo DuckDB. En modo escritura DuckDB crea el archivo
|
|
si no existe. Un directorio inexistente o un lock de otro proceso
|
|
devuelve {status:'error', ...}.
|
|
sql: sentencia SQL de escritura (INSERT/UPDATE/DELETE/DDL). Usa el
|
|
marcador `?` para parametros posicionales.
|
|
params: lista de parametros posicionales para el SQL en orden. None
|
|
(default) significa sin parametros.
|
|
|
|
Returns:
|
|
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}.
|
|
"""
|
|
conn = None
|
|
try:
|
|
conn = __import__("duckdb").connect(db_path)
|
|
cursor = conn.execute(sql, params if params is not None else [])
|
|
rowcount = _affected_rowcount(cursor)
|
|
# DuckDB autocommitea por defecto, pero llamar a commit es seguro e
|
|
# idempotente: garantiza la durabilidad de la escritura.
|
|
conn.commit()
|
|
return {"status": "ok", "rowcount": rowcount}
|
|
except Exception as e: # noqa: BLE001
|
|
return {"status": "error", "error": str(e)}
|
|
finally:
|
|
if conn is not None:
|
|
conn.close()
|