"""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()