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,85 @@
|
||||
"""Tests para duckdb_execute."""
|
||||
|
||||
import duckdb
|
||||
import pytest
|
||||
|
||||
from .duckdb_execute import duckdb_execute
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db(tmp_path):
|
||||
"""Crea una base DuckDB temporal con una tabla vacia y devuelve su path."""
|
||||
path = str(tmp_path / "test.duckdb")
|
||||
con = duckdb.connect(path)
|
||||
con.execute("CREATE TABLE t (id INTEGER, name VARCHAR)")
|
||||
con.close()
|
||||
return path
|
||||
|
||||
|
||||
def _read_rows(path: str) -> list:
|
||||
"""Relee la tabla t en una conexion read_only y devuelve las filas."""
|
||||
con = duckdb.connect(path, read_only=True)
|
||||
try:
|
||||
return con.execute("SELECT id, name FROM t ORDER BY id").fetchall()
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
def test_insert_devuelve_status_ok_y_persiste(db):
|
||||
res = duckdb_execute(
|
||||
db,
|
||||
"INSERT INTO t VALUES (?, ?), (?, ?), (?, ?)",
|
||||
params=[1, "a", 2, "b", 3, "c"],
|
||||
)
|
||||
assert res["status"] == "ok"
|
||||
assert res["rowcount"] == 3
|
||||
# Releemos para confirmar el efecto en disco.
|
||||
assert _read_rows(db) == [(1, "a"), (2, "b"), (3, "c")]
|
||||
|
||||
|
||||
def test_update_afecta_filas_y_persiste(db):
|
||||
duckdb_execute(db, "INSERT INTO t VALUES (1,'a'),(2,'b'),(3,'c')")
|
||||
res = duckdb_execute(db, "UPDATE t SET name = ? WHERE id <= ?", params=["x", 2])
|
||||
assert res["status"] == "ok"
|
||||
assert res["rowcount"] == 2
|
||||
assert _read_rows(db) == [(1, "x"), (2, "x"), (3, "c")]
|
||||
|
||||
|
||||
def test_delete_afecta_filas_y_persiste(db):
|
||||
duckdb_execute(db, "INSERT INTO t VALUES (1,'a'),(2,'b'),(3,'c')")
|
||||
res = duckdb_execute(db, "DELETE FROM t WHERE id = ?", params=[3])
|
||||
assert res["status"] == "ok"
|
||||
assert res["rowcount"] == 1
|
||||
assert _read_rows(db) == [(1, "a"), (2, "b")]
|
||||
|
||||
|
||||
def test_ddl_create_table_status_ok(db):
|
||||
res = duckdb_execute(db, "CREATE TABLE u (x INTEGER)")
|
||||
assert res["status"] == "ok"
|
||||
# DDL no reporta filas: rowcount queda en -1, no falla.
|
||||
assert res["rowcount"] == -1
|
||||
# Confirmamos que la tabla existe insertando en ella.
|
||||
res2 = duckdb_execute(db, "INSERT INTO u VALUES (42)")
|
||||
assert res2["status"] == "ok"
|
||||
con = duckdb.connect(db, read_only=True)
|
||||
try:
|
||||
assert con.execute("SELECT x FROM u").fetchall() == [(42,)]
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
def test_crea_la_base_si_no_existe(tmp_path):
|
||||
path = str(tmp_path / "nueva.duckdb")
|
||||
res = duckdb_execute(path, "CREATE TABLE nueva (a INTEGER)")
|
||||
assert res["status"] == "ok"
|
||||
res2 = duckdb_execute(path, "INSERT INTO nueva VALUES (7)")
|
||||
assert res2["status"] == "ok"
|
||||
assert res2["rowcount"] == 1
|
||||
|
||||
|
||||
def test_sql_invalido_devuelve_status_error(db):
|
||||
res = duckdb_execute(db, "INSERT INTO tabla_que_no_existe VALUES (1)")
|
||||
assert res["status"] == "error"
|
||||
assert "error" in res
|
||||
assert isinstance(res["error"], str) and res["error"]
|
||||
# La funcion no lanza: el flujo del test llega hasta aqui sin excepcion.
|
||||
Reference in New Issue
Block a user