refactor(db)!: adopt project-routed pooled databases
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.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from openpyxl import load_workbook
|
||||
|
||||
from app.native.wndb import s42_sensor_placement
|
||||
from app.infra.db.postgresql import sensor_placement as sensor_placement_repository
|
||||
from app.services import sensor_placement
|
||||
|
||||
|
||||
@@ -15,180 +16,121 @@ def _mock_project_cursor(monkeypatch):
|
||||
connection_context = MagicMock()
|
||||
connection_context.__enter__.return_value = connection
|
||||
monkeypatch.setattr(
|
||||
s42_sensor_placement,
|
||||
sensor_placement_repository,
|
||||
"project_connection",
|
||||
lambda _network: connection_context,
|
||||
)
|
||||
return cursor
|
||||
|
||||
|
||||
def test_build_workbook_contains_engineering_columns(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
sensor_placement,
|
||||
"_sensor_points",
|
||||
lambda network, locations: [
|
||||
{
|
||||
"node_id": "J1",
|
||||
"max_pipe_diameter": 400.0,
|
||||
"project_x": 13500000.0,
|
||||
"project_y": 3600000.0,
|
||||
"map_x": 13500000.0,
|
||||
"map_y": 3600000.0,
|
||||
"longitude": 121.0,
|
||||
"latitude": 31.0,
|
||||
"elevation": 4.5,
|
||||
}
|
||||
],
|
||||
)
|
||||
scheme = {
|
||||
"id": 7,
|
||||
"scheme_name": "北区测压点",
|
||||
"sensor_number": 2,
|
||||
def _run() -> dict:
|
||||
return {
|
||||
"run_id": uuid4(),
|
||||
"name": "北区测压点",
|
||||
"sensor_count": 2,
|
||||
"min_diameter": 300,
|
||||
"username": "alice",
|
||||
"create_time": datetime(2026, 7, 30, tzinfo=timezone.utc),
|
||||
"sensor_location": ["J1", "J2"],
|
||||
"created_by": "alice",
|
||||
"created_at": datetime(2026, 7, 30, tzinfo=timezone.utc),
|
||||
"status": "completed",
|
||||
"sensor_locations": ["J1", "J2"],
|
||||
}
|
||||
|
||||
|
||||
def _point() -> dict:
|
||||
return {
|
||||
"node_id": "J1",
|
||||
"max_pipe_diameter": 400.0,
|
||||
"project_x": 3038.94,
|
||||
"project_y": -34446.59,
|
||||
"map_x": 13525191.530279,
|
||||
"map_y": 3622984.760237,
|
||||
"longitude": 121.498863,
|
||||
"latitude": 30.924784,
|
||||
"elevation": 4.5,
|
||||
}
|
||||
|
||||
|
||||
def test_build_workbook_uses_analysis_run_metadata(monkeypatch):
|
||||
monkeypatch.setattr(sensor_placement, "_sensor_points", lambda *_: [_point()])
|
||||
output = sensor_placement.build_sensor_placement_workbook(
|
||||
network="tjwater",
|
||||
scheme=scheme,
|
||||
scheme=_run(),
|
||||
sensor_location=["J1"],
|
||||
adjustment_status={"J1": "replaced"},
|
||||
)
|
||||
workbook = load_workbook(output)
|
||||
|
||||
assert workbook.sheetnames == ["方案信息", "监测点清单"]
|
||||
headers = [cell.value for cell in workbook["监测点清单"][1]]
|
||||
assert headers == [
|
||||
"序号",
|
||||
"节点 ID",
|
||||
"经度",
|
||||
"纬度",
|
||||
"工程 X",
|
||||
"工程 Y",
|
||||
"地图 X",
|
||||
"地图 Y",
|
||||
"高程",
|
||||
"调整状态",
|
||||
]
|
||||
assert workbook["监测点清单"]["J2"].value == "替换"
|
||||
assert workbook["方案信息"]["B8"].value == "未保存草稿"
|
||||
|
||||
|
||||
def test_candidate_keeps_engineering_coordinates_and_transforms_map_coordinates(
|
||||
monkeypatch,
|
||||
):
|
||||
def test_candidate_transforms_map_coordinates(monkeypatch):
|
||||
row = _point().copy()
|
||||
row.pop("longitude")
|
||||
row.pop("latitude")
|
||||
monkeypatch.setattr(
|
||||
sensor_placement.wndb,
|
||||
sensor_placement.sensor_placement_repository,
|
||||
"get_sensor_placement_nodes",
|
||||
lambda network, node_ids: [
|
||||
{
|
||||
"node_id": "J1",
|
||||
"max_pipe_diameter": 400.0,
|
||||
"project_x": 3038.94,
|
||||
"project_y": -34446.59,
|
||||
"map_x": 13525191.530279,
|
||||
"map_y": 3622984.760237,
|
||||
"elevation": 4.5,
|
||||
}
|
||||
],
|
||||
lambda network, node_ids: [row],
|
||||
)
|
||||
|
||||
point = sensor_placement.get_sensor_placement_candidate("tjwater", "J1")
|
||||
|
||||
assert point["project_x"] == 3038.94
|
||||
assert point["project_y"] == -34446.59
|
||||
assert point["max_pipe_diameter"] == 400.0
|
||||
assert point["longitude"] == pytest.approx(121.498863, abs=1e-6)
|
||||
assert point["latitude"] == pytest.approx(30.924784, abs=1e-6)
|
||||
|
||||
|
||||
def test_update_validates_nodes_before_write(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
sensor_placement.wndb,
|
||||
sensor_placement.sensor_placement_repository,
|
||||
"get_sensor_placement_nodes",
|
||||
lambda network, node_ids: [],
|
||||
)
|
||||
|
||||
try:
|
||||
sensor_placement.update_sensor_placement_scheme(
|
||||
with pytest.raises(sensor_placement.SensorPlacementValidationError, match="missing"):
|
||||
sensor_placement.update_sensor_placement_run(
|
||||
"tjwater",
|
||||
7,
|
||||
expected_sensor_location=["J1"],
|
||||
sensor_location=["missing"],
|
||||
uuid4(),
|
||||
expected_sensor_locations=["J1"],
|
||||
sensor_locations=["missing"],
|
||||
)
|
||||
except sensor_placement.SensorPlacementValidationError as exc:
|
||||
assert "missing" in str(exc)
|
||||
else:
|
||||
raise AssertionError("expected invalid node to be rejected")
|
||||
|
||||
|
||||
def test_sensor_nodes_use_materialized_web_mercator_geometry(monkeypatch):
|
||||
def test_sensor_nodes_query_uses_new_network_and_gis_schemas(monkeypatch):
|
||||
cursor = _mock_project_cursor(monkeypatch)
|
||||
cursor.fetchall.return_value = []
|
||||
|
||||
s42_sensor_placement.get_sensor_placement_nodes("tjwater", ["J1"])
|
||||
sensor_placement_repository.get_sensor_placement_nodes("tjwater", ["J1"])
|
||||
|
||||
query = cursor.execute.call_args.args[0]
|
||||
assert "geo_junctions_mat" in query
|
||||
assert "ST_X(c.coord)" in query
|
||||
assert "ST_Y(c.coord)" in query
|
||||
assert "ST_X(gj.geom)" in query
|
||||
assert "ST_Y(gj.geom)" in query
|
||||
assert "MAX(diameter) AS max_pipe_diameter" in query
|
||||
assert "network.pipes" in query
|
||||
assert "network.links" in query
|
||||
assert "gis.node_geometries" in query
|
||||
assert "ST_Transform(g.geom, 3857)" in query
|
||||
assert cursor.execute.call_args.args[1] == (["J1"], ["J1"], ["J1"])
|
||||
|
||||
|
||||
def test_workbook_escapes_formula_in_scheme_metadata(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
sensor_placement,
|
||||
"_sensor_points",
|
||||
lambda network, locations: [
|
||||
{
|
||||
"node_id": "J1",
|
||||
"max_pipe_diameter": 400.0,
|
||||
"project_x": 3038.94,
|
||||
"project_y": -34446.59,
|
||||
"map_x": 13525191.53,
|
||||
"map_y": 3622984.76,
|
||||
"longitude": 121.49,
|
||||
"latitude": 30.92,
|
||||
"elevation": 4.5,
|
||||
}
|
||||
],
|
||||
)
|
||||
output = sensor_placement.build_sensor_placement_workbook(
|
||||
network="tjwater",
|
||||
scheme={
|
||||
"scheme_name": "=1+1",
|
||||
"sensor_location": ["J1"],
|
||||
"min_diameter": 300,
|
||||
"username": "alice",
|
||||
"create_time": datetime(2026, 7, 30, tzinfo=timezone.utc),
|
||||
},
|
||||
sensor_location=["J1"],
|
||||
adjustment_status={},
|
||||
)
|
||||
|
||||
workbook = load_workbook(output, data_only=False)
|
||||
assert workbook["方案信息"]["B2"].value == "'=1+1"
|
||||
assert workbook["方案信息"]["B2"].data_type == "s"
|
||||
|
||||
|
||||
def test_create_sensor_placement_returns_inserted_record(monkeypatch):
|
||||
def test_create_sensor_placement_writes_run_and_result_atomically(monkeypatch):
|
||||
cursor = _mock_project_cursor(monkeypatch)
|
||||
cursor.fetchone.return_value = {"id": 7, "sensor_location": ["J1", "J2"]}
|
||||
created_at = datetime(2026, 8, 24, tzinfo=timezone.utc)
|
||||
cursor.fetchone.return_value = {
|
||||
"run_id": uuid4(),
|
||||
"name": "北区测压点",
|
||||
"created_by": "alice",
|
||||
"created_at": created_at,
|
||||
"status": "completed",
|
||||
}
|
||||
|
||||
created = s42_sensor_placement.create_sensor_placement(
|
||||
created = sensor_placement_repository.create_sensor_placement(
|
||||
"tjwater",
|
||||
scheme_name="北区测压点",
|
||||
run_name="北区测压点",
|
||||
min_diameter=300,
|
||||
username="alice",
|
||||
sensor_location=["J1", "J2"],
|
||||
created_by="alice",
|
||||
sensor_locations=["J1", "J2"],
|
||||
)
|
||||
|
||||
assert created["id"] == 7
|
||||
query, parameters = cursor.execute.call_args.args
|
||||
assert "INSERT INTO sensor_placement" in query
|
||||
assert parameters == ("北区测压点", 2, 300, "alice", ["J1", "J2"])
|
||||
assert created["sensor_count"] == 2
|
||||
statements = [call.args[0] for call in cursor.execute.call_args_list]
|
||||
assert "INSERT INTO analysis.runs" in statements[0]
|
||||
assert "INSERT INTO analysis.results" in statements[1]
|
||||
|
||||
Reference in New Issue
Block a user