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:
2026-08-28 11:37:36 +08:00
parent b74799a39d
commit 9b095c7439
34 changed files with 859 additions and 921 deletions
+65 -233
View File
@@ -1,32 +1,21 @@
from datetime import date, datetime
from datetime import datetime
from typing import Any
from uuid import UUID, uuid4
from psycopg.types.json import Jsonb
from app.native.wndb.core.connection import project_connection
from app.infra.db.postgresql.analysis import AnalysisRepository
from app.native.wndb.core.connection import project_connection, project_transaction
from app.services.time_api import parse_utc_time
def scheme_name_exists(name: str, scheme_name: str) -> bool:
with project_connection(name) as conn, conn.cursor() as cur:
cur.execute(
"select exists(select 1 from analysis.runs where name = %s)",
(scheme_name,),
)
row = cur.fetchone()
return bool(row and row[0])
def store_scheme_info(
name: str,
scheme_name: str,
scheme_type: str,
username: str,
scheme_start_time: datetime | str,
scheme_detail: dict,
scheme_detail: dict[str, Any],
) -> UUID:
"""Create one completed, immutable analysis run."""
"""Create one completed analysis run; its name remains a display label."""
return create_analysis_run(
name=name,
scheme_name=scheme_name,
@@ -44,29 +33,56 @@ def create_analysis_run(
scheme_type: str,
username: str,
scheme_start_time: datetime | str,
scheme_detail: dict,
scheme_detail: dict[str, Any],
*,
status: str = "running",
) -> UUID:
"""Create a distinct execution record; names are labels, not identities."""
started_at = parse_utc_time(scheme_start_time, field_name="scheme_start_time")
run_id = uuid4()
with project_connection(name) as conn, conn.cursor() as cur:
cur.execute(
"""
insert into analysis.runs
(run_id, name, run_type, created_by, created_at, started_at, status, parameters)
values (%s, %s, %s, %s, now(), %s, %s, %s)
""",
(
run_id,
scheme_name,
scheme_type,
username,
started_at,
status,
Jsonb(scheme_detail),
),
with project_connection(name) as conn:
AnalysisRepository.create_run_sync(
conn,
run_id=run_id,
name=scheme_name,
run_type=scheme_type,
created_by=username,
started_at=started_at,
status=status,
parameters=scheme_detail,
)
return run_id
def store_analysis_run_with_result(
*,
name: str,
scheme_name: str,
scheme_type: str,
username: str,
scheme_start_time: datetime | str,
scheme_detail: dict[str, Any],
result_type: str,
result_payload: dict[str, Any],
) -> UUID:
"""Atomically persist one BizDB run and its non-timeseries result."""
started_at = parse_utc_time(scheme_start_time, field_name="scheme_start_time")
run_id = uuid4()
with project_transaction(name) as conn:
AnalysisRepository.create_run_sync(
conn,
run_id=run_id,
name=scheme_name,
run_type=scheme_type,
created_by=username,
started_at=started_at,
status="completed",
parameters=scheme_detail,
)
AnalysisRepository.insert_result_sync(
conn,
run_id,
result_type=result_type,
payload=result_payload,
)
return run_id
@@ -77,208 +93,24 @@ def update_analysis_run(
*,
status: str,
username: str,
scheme_detail: dict,
scheme_detail: dict[str, Any],
) -> None:
"""Update lifecycle state and metadata for one execution identity."""
with project_connection(name) as conn, conn.cursor() as cur:
cur.execute(
"""
update analysis.runs
set created_by = %s, status = %s, parameters = %s
where run_id = %s
""",
(username, status, Jsonb(scheme_detail), run_id),
with project_connection(name) as conn:
AnalysisRepository.update_run_sync(
conn,
run_id,
status=status,
created_by=username,
parameters=scheme_detail,
)
if cur.rowcount != 1:
raise LookupError(f"analysis run {run_id} does not exist")
def _run_row(row: dict[str, Any]) -> dict[str, Any]:
parameters = row.get("parameters") if isinstance(row.get("parameters"), dict) else {}
return {
"run_id": row["run_id"],
"name": row["name"],
"run_type": row["run_type"],
"created_by": row["created_by"],
"created_at": row["created_at"],
"started_at": row["started_at"],
"status": row["status"],
"parameters": parameters,
}
def _list_runs(
name: str,
run_type: str | None = None,
query_date: date | None = None,
) -> list[dict[str, Any]]:
clauses: list[str] = []
params: list[Any] = []
if run_type:
clauses.append("run_type = %s")
params.append(run_type)
if query_date is not None:
clauses.append("created_at::date = %s")
params.append(query_date)
where = f"where {' and '.join(clauses)}" if clauses else ""
with project_connection(name) as conn, conn.cursor() as cur:
cur.execute(
f"select run_id, name, run_type, created_by, created_at, started_at, status, parameters from analysis.runs {where} order by created_at desc",
params,
)
return [_run_row(row) for row in cur.fetchall()]
def query_scheme_list(
name: str,
scheme_type: str | None = None,
query_date: date | None = None,
) -> list[dict[str, Any]]:
return _list_runs(name, scheme_type, query_date)
def _get_run_by_name(
name: str,
run_name: str,
run_type: str | None = None,
) -> dict[str, Any]:
params: list[Any] = [run_name]
type_clause = ""
if run_type:
type_clause = "and run_type = %s"
params.append(run_type)
with project_connection(name) as conn, conn.cursor() as cur:
cur.execute(
f"""
select run_id, name, run_type, created_by, created_at, started_at,
status, parameters
from analysis.runs
where name = %s {type_clause}
order by created_at desc
limit 1
""",
params,
)
row = cur.fetchone()
return _run_row(row) if row else {}
def get_analysis_run(name: str, run_id: UUID) -> dict[str, Any]:
with project_connection(name) as conn, conn.cursor() as cur:
cur.execute(
"""
select run_id, name, run_type, created_by, created_at, started_at,
status, parameters
from analysis.runs
where run_id = %s
""",
(run_id,),
)
row = cur.fetchone()
return _run_row(row) if row else {}
def query_scheme_detail(
name: str,
scheme_name: str,
scheme_type: str | None = None,
) -> dict[str, Any]:
return _get_run_by_name(name, scheme_name, scheme_type)
def store_leakage_identify_result(
name: str,
scheme_name: str,
network: str,
sensor_nodes: list[str],
result_rows: list[dict],
node_area_map: dict[str, str],
areas: list[dict],
drawing_payload: dict | None = None,
run_status: str = "completed",
error_message: str | None = None,
) -> None:
run = _get_run_by_name(name, scheme_name, "dma_leak_identification")
if not run:
raise LookupError(f"analysis run {scheme_name!r} does not exist")
payload = {
"network": network,
"run_status": run_status,
"error_message": error_message,
"sensor_nodes": sensor_nodes,
"rows": result_rows,
"node_area_map": node_area_map,
"areas": areas,
"drawing_payload": drawing_payload or {},
}
with project_connection(name) as conn, conn.cursor() as cur:
cur.execute(
"insert into analysis.results (run_id, result_type, payload) values (%s, 'leakage_identification', %s)",
(run["run_id"], Jsonb(payload)),
)
def _list_typed_runs(
name: str,
network: str,
run_type: str,
query_date: date | None,
) -> list[dict[str, Any]]:
rows = _list_runs(name, run_type, query_date)
return [
row
for row in rows
if not network or row["parameters"].get("network") in (None, network)
]
def _typed_run_detail(name: str, run_name: str, run_type: str) -> dict[str, Any]:
run = _get_run_by_name(name, run_name, run_type)
if not run:
with project_connection(name) as conn:
row = AnalysisRepository.get_run_sync(conn, run_id)
if row is None:
return {}
with project_connection(name) as conn, conn.cursor() as cur:
cur.execute(
"select result_type, payload, created_at from analysis.results where run_id = %s order by created_at, result_id",
(run["run_id"],),
)
results = [dict(row) for row in cur.fetchall()]
return run | {"results": results}
def query_leakage_identify_schemes(
name: str,
network: str,
scheme_type: str = "dma_leak_identification",
query_date: date | None = None,
) -> list[dict[str, Any]]:
return _list_typed_runs(name, network, scheme_type, query_date)
def query_leakage_identify_scheme_detail(name: str, scheme_name: str) -> dict[str, Any]:
return _typed_run_detail(name, scheme_name, "dma_leak_identification")
def query_burst_location_schemes(
name: str,
network: str,
scheme_type: str = "burst_location",
query_date: date | None = None,
) -> list[dict[str, Any]]:
return _list_typed_runs(name, network, scheme_type, query_date)
def query_burst_location_scheme_detail(name: str, scheme_name: str) -> dict[str, Any]:
return _typed_run_detail(name, scheme_name, "burst_location")
def query_burst_detection_schemes(
name: str,
network: str,
scheme_type: str = "burst_detection",
query_date: date | None = None,
) -> list[dict[str, Any]]:
return _list_typed_runs(name, network, scheme_type, query_date)
def query_burst_detection_scheme_detail(name: str, scheme_name: str) -> dict[str, Any]:
return _typed_run_detail(name, scheme_name, "burst_detection")
parameters = row.get("parameters")
return dict(row) | {
"parameters": parameters if isinstance(parameters, dict) else {}
}