"""Tests para duckdb_query_readonly.""" import datetime import decimal import duckdb import pytest from .duckdb_query_readonly import duckdb_query_readonly @pytest.fixture def db(tmp_path): """Crea una base DuckDB temporal con datos de ejemplo y devuelve su path.""" path = str(tmp_path / "test.duckdb") con = duckdb.connect(path) con.execute( "CREATE TABLE ventas (" " id INTEGER," " region VARCHAR," " total DECIMAL(10,2)," " fecha DATE" ")" ) con.execute( "INSERT INTO ventas VALUES " "(1, 'norte', 120.50, DATE '2026-01-01'), " "(2, 'sur', 80.00, DATE '2026-01-02'), " "(3, 'norte', 45.25, DATE '2026-01-03')" ) con.close() return path def test_query_ok_devuelve_filas_como_dicts(db): res = duckdb_query_readonly(db, "SELECT id, region FROM ventas ORDER BY id") assert res["status"] == "ok" assert res["columns"] == ["id", "region"] assert res["row_count"] == 3 assert res["truncated"] is False assert res["rows"][0] == {"id": 1, "region": "norte"} assert res["rows"][1] == {"id": 2, "region": "sur"} def test_query_con_params_posicionales(db): res = duckdb_query_readonly( db, "SELECT id FROM ventas WHERE region = ? ORDER BY id", params=["norte"], ) assert res["status"] == "ok" assert res["row_count"] == 2 assert [row["id"] for row in res["rows"]] == [1, 3] def test_sql_invalido_devuelve_status_error(db): res = duckdb_query_readonly(db, "SELECT * FROM tabla_que_no_existe") assert res["status"] == "error" assert "error" in res assert isinstance(res["error"], str) and res["error"] def test_db_inexistente_devuelve_status_error(tmp_path): missing = str(tmp_path / "no_existe.duckdb") res = duckdb_query_readonly(missing, "SELECT 1") assert res["status"] == "error" assert "error" in res def test_truncado_a_max_rows(db): res = duckdb_query_readonly(db, "SELECT id FROM ventas ORDER BY id", max_rows=2) assert res["status"] == "ok" assert res["row_count"] == 2 assert res["truncated"] is True assert [row["id"] for row in res["rows"]] == [1, 2] def test_valores_no_serializables_se_convierten(db): res = duckdb_query_readonly( db, "SELECT total, fecha FROM ventas WHERE id = ?", params=[1], ) assert res["status"] == "ok" row = res["rows"][0] # Decimal -> float assert isinstance(row["total"], float) assert row["total"] == pytest.approx(120.50) # date -> isoformat str assert isinstance(row["fecha"], str) assert row["fecha"] == "2026-01-01" # Verificamos que NO quedan tipos crudos no serializables. assert not isinstance(row["total"], decimal.Decimal) assert not isinstance(row["fecha"], datetime.date) def test_sandbox_por_defecto_bloquea_acceso_a_ficheros(db, tmp_path): """Por defecto (sandbox=True) la query no puede tocar el sistema de ficheros.""" csv = tmp_path / "externo.csv" csv.write_text("a\n1\n") res = duckdb_query_readonly(db, f"SELECT * FROM read_csv_auto('{csv}')") assert res["status"] == "error" assert ( "access" in res["error"].lower() or "permission" in res["error"].lower() ) # El SELECT normal sobre la propia base sigue funcionando con el sandbox. ok = duckdb_query_readonly(db, "SELECT count(*) AS n FROM ventas") assert ok["status"] == "ok" and ok["rows"][0]["n"] == 3 def test_sandbox_off_permite_leer_csv_del_disco(db, tmp_path): """Con sandbox=False (uso interno confiable) sí puede leer ficheros.""" csv = tmp_path / "externo.csv" csv.write_text("a\n1\n2\n") res = duckdb_query_readonly( db, f"SELECT count(*) AS n FROM read_csv_auto('{csv}')", sandbox=False ) assert res["status"] == "ok" assert res["rows"][0]["n"] == 2