refactor(db)!: clean up business SQL access
- make realtime replacement and analysis result writes transactional\n- consolidate SCADA repositories and remove process-global project state\n- validate SCADA batches and use indexed GIS-backed business queries\n\nBREAKING CHANGE: remove the public analysis result writer and the pipeline-health network_name query parameter.
This commit is contained in:
@@ -1,7 +1,40 @@
|
||||
from typing import Any
|
||||
from dataclasses import dataclass
|
||||
from types import MappingProxyType
|
||||
from typing import Any, Mapping
|
||||
|
||||
from psycopg import AsyncConnection
|
||||
|
||||
from app.native.wndb.core.database import read_all, try_read
|
||||
|
||||
|
||||
_SCADA_VIEW_SELECT = """
|
||||
SELECT id AS device_id, device_type, node_id, link_id, api_query_id,
|
||||
transmission_mode, transmission_frequency, reliability, x, y
|
||||
FROM gis.scada_devices
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScadaElementMappings:
|
||||
reservoirs: Mapping[str, str]
|
||||
tanks: Mapping[str, str]
|
||||
fixed_pumps: Mapping[str, str]
|
||||
variable_pumps: Mapping[str, str]
|
||||
pressure: Mapping[str, str]
|
||||
demand: Mapping[str, str]
|
||||
quality: Mapping[str, str]
|
||||
|
||||
|
||||
def _empty_mapping_groups() -> dict[str, dict[str, str]]:
|
||||
return {
|
||||
"reservoir_liquid_level": {},
|
||||
"tank_liquid_level": {},
|
||||
"fixed_pump": {},
|
||||
"variable_pump": {},
|
||||
"pressure": {},
|
||||
"demand": {},
|
||||
"quality": {},
|
||||
}
|
||||
|
||||
def _optional_text(value: Any) -> str | None:
|
||||
return str(value).strip() if value is not None else None
|
||||
@@ -15,6 +48,21 @@ def _optional_int(value: Any) -> int | None:
|
||||
return int(value) if value is not None else None
|
||||
|
||||
|
||||
def _device(record: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"device_id": str(record["device_id"]).strip(),
|
||||
"device_type": str(record["device_type"]).strip().lower(),
|
||||
"node_id": _optional_text(record["node_id"]),
|
||||
"link_id": _optional_text(record["link_id"]),
|
||||
"api_query_id": _optional_text(record["api_query_id"]),
|
||||
"transmission_mode": record["transmission_mode"],
|
||||
"transmission_frequency": record["transmission_frequency"],
|
||||
"reliability": _optional_int(record["reliability"]),
|
||||
"x": _optional_float(record["x"]),
|
||||
"y": _optional_float(record["y"]),
|
||||
}
|
||||
|
||||
|
||||
class ScadaInfoRepository:
|
||||
"""Read SCADA metadata from the current project's business database."""
|
||||
|
||||
@@ -22,35 +70,83 @@ class ScadaInfoRepository:
|
||||
async def get_scadas(conn: AsyncConnection) -> list[dict[str, Any]]:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(
|
||||
"""
|
||||
SELECT id AS device_id,
|
||||
device_type,
|
||||
node_id,
|
||||
link_id,
|
||||
api_query_id,
|
||||
transmission_mode,
|
||||
transmission_frequency,
|
||||
reliability,
|
||||
x,
|
||||
y
|
||||
FROM gis.scada_devices
|
||||
ORDER BY id
|
||||
"""
|
||||
_SCADA_VIEW_SELECT + " ORDER BY device_id"
|
||||
)
|
||||
records = await cur.fetchall()
|
||||
|
||||
return [
|
||||
{
|
||||
"device_id": str(record["device_id"]).strip(),
|
||||
"device_type": str(record["device_type"]).strip().lower(),
|
||||
"node_id": _optional_text(record["node_id"]),
|
||||
"link_id": _optional_text(record["link_id"]),
|
||||
"api_query_id": _optional_text(record["api_query_id"]),
|
||||
"transmission_mode": record["transmission_mode"],
|
||||
"transmission_frequency": record["transmission_frequency"],
|
||||
"reliability": _optional_int(record["reliability"]),
|
||||
"x": _optional_float(record["x"]),
|
||||
"y": _optional_float(record["y"]),
|
||||
}
|
||||
for record in records
|
||||
]
|
||||
return [_device(record) for record in records]
|
||||
|
||||
@staticmethod
|
||||
async def get_existing_device_ids(
|
||||
conn: AsyncConnection, device_ids: list[str]
|
||||
) -> set[str]:
|
||||
if not device_ids:
|
||||
return set()
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(
|
||||
"SELECT device_id FROM asset.scada_devices WHERE device_id = ANY(%s)",
|
||||
(device_ids,),
|
||||
)
|
||||
return {str(row["device_id"]).strip() for row in await cur.fetchall()}
|
||||
|
||||
|
||||
def get_scada_info_schema(name: str) -> dict[str, dict[str, Any]]:
|
||||
return {
|
||||
"device_id": {"type": "str", "optional": False, "readonly": True},
|
||||
"device_type": {"type": "str", "optional": False, "readonly": True},
|
||||
"node_id": {"type": "str", "optional": True, "readonly": True},
|
||||
"link_id": {"type": "str", "optional": True, "readonly": True},
|
||||
"api_query_id": {"type": "str", "optional": True, "readonly": True},
|
||||
"transmission_mode": {"type": "str", "optional": False, "readonly": True},
|
||||
"transmission_frequency": {"type": "str", "optional": False, "readonly": True},
|
||||
"reliability": {"type": "int", "optional": False, "readonly": True},
|
||||
"x": {"type": "float", "optional": True, "readonly": True},
|
||||
"y": {"type": "float", "optional": True, "readonly": True},
|
||||
}
|
||||
|
||||
|
||||
def get_scada_info(name: str, device_id: str) -> dict[str, Any]:
|
||||
row = try_read(
|
||||
name,
|
||||
_SCADA_VIEW_SELECT + " WHERE id = %s",
|
||||
(device_id,),
|
||||
)
|
||||
return _device(row) if row else {}
|
||||
|
||||
|
||||
def get_all_scada_info(name: str) -> list[dict[str, Any]]:
|
||||
return [
|
||||
_device(row)
|
||||
for row in read_all(name, _SCADA_VIEW_SELECT + " ORDER BY device_id")
|
||||
]
|
||||
|
||||
|
||||
def load_realtime_element_mappings(name: str) -> ScadaElementMappings:
|
||||
"""Load one project-local immutable SCADA-to-model mapping snapshot."""
|
||||
groups = _empty_mapping_groups()
|
||||
rows = read_all(
|
||||
name,
|
||||
"""
|
||||
SELECT device_type, COALESCE(node_id, link_id) AS element_id,
|
||||
api_query_id
|
||||
FROM asset.scada_devices
|
||||
WHERE transmission_mode = 'realtime'
|
||||
AND api_query_id IS NOT NULL
|
||||
""",
|
||||
)
|
||||
for row in rows:
|
||||
group = groups.get(str(row["device_type"]).strip().lower())
|
||||
if group is not None:
|
||||
group[str(row["element_id"]).strip()] = str(row["api_query_id"]).strip()
|
||||
immutable = {
|
||||
name: MappingProxyType(values.copy()) for name, values in groups.items()
|
||||
}
|
||||
return ScadaElementMappings(
|
||||
reservoirs=immutable["reservoir_liquid_level"],
|
||||
tanks=immutable["tank_liquid_level"],
|
||||
fixed_pumps=immutable["fixed_pump"],
|
||||
variable_pumps=immutable["variable_pump"],
|
||||
pressure=immutable["pressure"],
|
||||
demand=immutable["demand"],
|
||||
quality=immutable["quality"],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user