Files
fn_registry/python/functions/infra/duckdb_upsert_test.py
T
egutierrez 1c4a4b9259 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>
2026-06-13 00:33:12 +02:00

134 lines
3.8 KiB
Python

"""Tests para duckdb_upsert."""
import os
import sys
import tempfile
import duckdb
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from infra.duckdb_upsert import duckdb_upsert # noqa: E402
def _fresh_db():
"""Crea un .duckdb temporal con tabla t(k PK, a, b) y devuelve su path."""
fd, path = tempfile.mkstemp(suffix=".duckdb")
os.close(fd)
os.remove(path) # DuckDB crea el archivo limpio.
con = duckdb.connect(path)
con.execute("CREATE TABLE t (k INTEGER PRIMARY KEY, a VARCHAR, b VARCHAR)")
con.close()
return path
def _select_row(path, k):
con = duckdb.connect(path, read_only=True)
try:
return con.execute("SELECT k, a, b FROM t WHERE k = ?", [k]).fetchone()
finally:
con.close()
def test_upsert_fila_nueva_inserta():
path = _fresh_db()
try:
res = duckdb_upsert(
path, "t", [{"k": 1, "a": "a1", "b": "b1"}], key_cols=["k"]
)
assert res == {"status": "ok", "inserted": 1, "updated": 0}
assert _select_row(path, 1) == (1, "a1", "b1")
finally:
os.remove(path)
def test_update_cols_selectivo_no_pisa_columnas_excluidas():
path = _fresh_db()
try:
duckdb_upsert(path, "t", [{"k": 1, "a": "a1", "b": "b1"}], key_cols=["k"])
# Re-upsert de la misma k cambiando a y b en el dict, pero solo
# autorizando actualizar 'a'. 'b' debe conservar el valor viejo.
res = duckdb_upsert(
path,
"t",
[{"k": 1, "a": "a2", "b": "b2"}],
key_cols=["k"],
update_cols=["a"],
)
assert res == {"status": "ok", "inserted": 0, "updated": 1}
assert _select_row(path, 1) == (1, "a2", "b1") # a cambio, b NO
finally:
os.remove(path)
def test_update_cols_vacio_do_nothing_no_cambia_existente():
path = _fresh_db()
try:
duckdb_upsert(path, "t", [{"k": 1, "a": "a1", "b": "b1"}], key_cols=["k"])
res = duckdb_upsert(
path,
"t",
[{"k": 1, "a": "X", "b": "Y"}],
key_cols=["k"],
update_cols=[],
)
assert res == {"status": "ok", "inserted": 0, "updated": 1}
assert _select_row(path, 1) == (1, "a1", "b1") # intacta
finally:
os.remove(path)
def test_varias_filas_a_la_vez_mezcla_insert_y_update():
path = _fresh_db()
try:
duckdb_upsert(path, "t", [{"k": 1, "a": "a1", "b": "b1"}], key_cols=["k"])
res = duckdb_upsert(
path,
"t",
[
{"k": 1, "a": "a1b", "b": "b1b"}, # update
{"k": 2, "a": "a2", "b": "b2"}, # insert
{"k": 3, "a": "a3", "b": "b3"}, # insert
],
key_cols=["k"],
)
assert res == {"status": "ok", "inserted": 2, "updated": 1}
assert _select_row(path, 1) == (1, "a1b", "b1b")
assert _select_row(path, 2) == (2, "a2", "b2")
assert _select_row(path, 3) == (3, "a3", "b3")
finally:
os.remove(path)
def test_rows_vacio_devuelve_cero():
path = _fresh_db()
try:
res = duckdb_upsert(path, "t", [], key_cols=["k"])
assert res == {"status": "ok", "inserted": 0, "updated": 0}
finally:
os.remove(path)
def test_columnas_inconsistentes_devuelve_error():
path = _fresh_db()
try:
res = duckdb_upsert(
path,
"t",
[{"k": 1, "a": "a1", "b": "b1"}, {"k": 2, "a": "a2"}],
key_cols=["k"],
)
assert res["status"] == "error"
finally:
os.remove(path)
def test_identificador_invalido_devuelve_error():
path = _fresh_db()
try:
res = duckdb_upsert(
path, "t; DROP TABLE t", [{"k": 1, "a": "a1", "b": "b1"}], key_cols=["k"]
)
assert res["status"] == "error"
finally:
os.remove(path)