--- 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.