Reorganize WNDB by responsibility and remove legacy scheme endpoints.\n\nRoute analysis and time-series access through project pools, preserve transactional realtime replacement, and refresh GIS materialized views after writes.\n\nAdd database architecture documentation, live pooling coverage, API contract updates, and executable container verification.\n\nBREAKING CHANGE: legacy scheme APIs and flat app.native.wndb module imports are removed.
92 lines
2.4 KiB
Python
92 lines
2.4 KiB
Python
import asyncio
|
|
from datetime import datetime, timezone
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from app.infra.db.timescaledb.repositories.analysis import AnalysisResultsRepository
|
|
|
|
|
|
class _FakeCursor:
|
|
def __init__(self):
|
|
self.calls = []
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
async def execute(self, query, params):
|
|
self.calls.append((str(query), params))
|
|
|
|
async def fetchall(self):
|
|
return []
|
|
|
|
|
|
class _FakeConnection:
|
|
def __init__(self):
|
|
self.cursor_instance = _FakeCursor()
|
|
|
|
def cursor(self):
|
|
return self.cursor_instance
|
|
|
|
|
|
def test_prepare_simulation_rows_uses_run_timestep():
|
|
nodes, links = AnalysisResultsRepository.prepare_simulation_rows(
|
|
[{"node": "J1", "result": [{"pressure": 1.0}, {"pressure": 2.0}]}],
|
|
[{"link": "P1", "result": [{"flow": 3.0}, {"flow": 4.0}]}],
|
|
"2026-08-24T08:00:00+08:00",
|
|
num_periods=2,
|
|
result_timestep_seconds=900,
|
|
)
|
|
|
|
assert [row["time"] for row in nodes] == [
|
|
datetime(2026, 8, 24, 0, 0, tzinfo=timezone.utc),
|
|
datetime(2026, 8, 24, 0, 15, tzinfo=timezone.utc),
|
|
]
|
|
assert nodes[0]["node_id"] == "J1"
|
|
assert links[1]["link_id"] == "P1"
|
|
|
|
|
|
def test_node_series_rejects_unknown_field():
|
|
with pytest.raises(ValueError, match="invalid node result field"):
|
|
asyncio.run(
|
|
AnalysisResultsRepository.get_node_series(
|
|
_FakeConnection(),
|
|
uuid4(),
|
|
"J1",
|
|
datetime.now(timezone.utc),
|
|
datetime.now(timezone.utc),
|
|
"unknown",
|
|
)
|
|
)
|
|
|
|
|
|
def test_node_series_filters_by_run_and_node():
|
|
conn = _FakeConnection()
|
|
run_id = uuid4()
|
|
start = datetime(2026, 8, 24, tzinfo=timezone.utc)
|
|
end = datetime(2026, 8, 25, tzinfo=timezone.utc)
|
|
|
|
asyncio.run(
|
|
AnalysisResultsRepository.get_node_series(
|
|
conn, run_id, "J1", start, end, "pressure"
|
|
)
|
|
)
|
|
|
|
query, params = conn.cursor_instance.calls[0]
|
|
assert "analysis.node_results" in query
|
|
assert params == (run_id, "J1", start, end)
|
|
|
|
|
|
def test_analysis_store_locks_run_before_empty_check():
|
|
cursor = _FakeCursor()
|
|
run_id = uuid4()
|
|
|
|
asyncio.run(AnalysisResultsRepository._lock_run(cursor, run_id))
|
|
|
|
query, params = cursor.calls[0]
|
|
assert "pg_advisory_xact_lock" in query
|
|
assert params == (run_id,)
|