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:
2026-08-25 18:35:05 +08:00
parent fdbcc5c033
commit fa188af0b1
181 changed files with 8446 additions and 33546 deletions
+282
View File
@@ -0,0 +1,282 @@
from typing import Any
from psycopg import sql
from psycopg.rows import Row, dict_row
from ..core.connection import project_connection
from ..core.database import read
_NODE = "network.nodes"
_LINK = "network.links"
_CURVE = "network.curves"
_PATTERN = "network.patterns"
_REGION = "gis.regions"
JUNCTION = "junction"
RESERVOIR = "reservoir"
TANK = "tank"
PIPE = "pipe"
PUMP = "pump"
VALVE = "valve"
PATTERN = "pattern"
CURVE = "curve"
REGION = "region"
ELEMENT_TYPES: dict[str, int] = {
RESERVOIR: 0,
TANK: 1,
JUNCTION: 2,
PIPE: 3,
PUMP: 4,
VALVE: 5,
}
def _table_identifier(table: str):
return sql.Identifier(*table.split("."))
def _get_from(name: str, element_id: str, table: str) -> Row | None:
query = sql.SQL("SELECT * FROM {} WHERE id = %s").format(
_table_identifier(table)
)
with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
cur.execute(query, (element_id,))
return cur.fetchone()
def is_node(name: str, element_id: str) -> bool:
return _get_from(name, element_id, _NODE) is not None
def is_junction(name: str, element_id: str) -> bool:
row = _get_from(name, element_id, _NODE)
return row is not None and row["node_type"] == JUNCTION
def is_reservoir(name: str, element_id: str) -> bool:
row = _get_from(name, element_id, _NODE)
return row is not None and row["node_type"] == RESERVOIR
def is_tank(name: str, element_id: str) -> bool:
row = _get_from(name, element_id, _NODE)
return row is not None and row["node_type"] == TANK
def is_link(name: str, element_id: str) -> bool:
return _get_from(name, element_id, _LINK) is not None
def is_pipe(name: str, element_id: str) -> bool:
row = _get_from(name, element_id, _LINK)
return row is not None and row["link_type"] == PIPE
def is_pump(name: str, element_id: str) -> bool:
row = _get_from(name, element_id, _LINK)
return row is not None and row["link_type"] == PUMP
def is_valve(name: str, element_id: str) -> bool:
row = _get_from(name, element_id, _LINK)
return row is not None and row["link_type"] == VALVE
def get_node_type(name: str, node_id: str) -> str:
row = _get_from(name, node_id, _NODE)
if row is None:
raise LookupError(node_id)
return row["node_type"]
def get_link_type(name: str, link_id: str) -> str:
row = _get_from(name, link_id, _LINK)
if row is None:
raise LookupError(link_id)
return row["link_type"]
def get_element_type(name: str, element_id: str) -> str | None:
if is_node(name, element_id):
return get_node_type(name, element_id)
if is_link(name, element_id):
return get_link_type(name, element_id)
return None
def get_element_type_value(name: str, element_id: str) -> int:
element_type = get_element_type(name, element_id)
if element_type is None:
raise LookupError(element_id)
return ELEMENT_TYPES[element_type]
def is_curve(name: str, element_id: str) -> bool:
return _get_from(name, element_id, _CURVE) is not None
def is_pattern(name: str, element_id: str) -> bool:
return _get_from(name, element_id, _PATTERN) is not None
def is_region(name: str, element_id: str) -> bool:
return _get_from(name, element_id, _REGION) is not None
def _get_all(name: str, table: str) -> list[str]:
query = sql.SQL("SELECT id FROM {} ORDER BY id").format(_table_identifier(table))
with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
cur.execute(query)
return [row["id"] for row in cur]
def _get_nodes_by_type(name: str, node_type: str) -> list[str]:
rows = read_all_typed(
name,
"SELECT id FROM network.nodes WHERE node_type = %s ORDER BY id",
(node_type,),
)
return [row["id"] for row in rows]
def _get_links_by_type(name: str, link_type: str) -> list[str]:
rows = read_all_typed(
name,
"SELECT id FROM network.links WHERE link_type = %s ORDER BY id",
(link_type,),
)
return [row["id"] for row in rows]
def read_all_typed(name: str, query: str, params: tuple[Any, ...]) -> list[Row]:
with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
cur.execute(query, params)
return cur.fetchall()
def get_nodes(name: str) -> list[str]:
return _get_all(name, _NODE)
def get_nodes_id_and_type(name: str) -> dict[str, str]:
rows = read_all_typed(name, "SELECT id, node_type FROM network.nodes", ())
return {row["id"]: row["node_type"] for row in rows}
def get_major_nodes(name: str, diameter: int) -> list[str]:
rows = read_all_typed(
name,
"""
SELECT DISTINCT endpoint
FROM network.links AS l
JOIN network.pipes AS p ON p.link_id = l.id
CROSS JOIN LATERAL (VALUES (l.start_node_id), (l.end_node_id)) AS e(endpoint)
WHERE p.diameter > %s
""",
(diameter,),
)
return [row["endpoint"] for row in rows]
def get_junctions(name: str) -> list[str]:
return _get_nodes_by_type(name, JUNCTION)
def get_reservoirs(name: str) -> list[str]:
return _get_nodes_by_type(name, RESERVOIR)
def get_tanks(name: str) -> list[str]:
return _get_nodes_by_type(name, TANK)
def get_links(name: str) -> list[str]:
return _get_all(name, _LINK)
def get_links_id_and_type(name: str) -> dict[str, str]:
rows = read_all_typed(name, "SELECT id, link_type FROM network.links", ())
return {row["id"]: row["link_type"] for row in rows}
def get_major_pipes(name: str, diameter: int) -> list[str]:
rows = read_all_typed(
name,
"SELECT link_id FROM network.pipes WHERE diameter > %s ORDER BY link_id",
(diameter,),
)
return [row["link_id"] for row in rows]
def get_pipes(name: str) -> list[str]:
return _get_links_by_type(name, PIPE)
def get_pumps(name: str) -> list[str]:
return _get_links_by_type(name, PUMP)
def get_valves(name: str) -> list[str]:
return _get_links_by_type(name, VALVE)
def get_curves(name: str) -> list[str]:
return _get_all(name, _CURVE)
def get_patterns(name: str) -> list[str]:
return _get_all(name, _PATTERN)
def get_regions(name: str) -> list[str]:
return _get_all(name, _REGION)
def get_node_links(name: str, node_id: str) -> list[str]:
rows = read_all_typed(
name,
"""
SELECT id FROM network.links
WHERE start_node_id = %s OR end_node_id = %s
ORDER BY id
""",
(node_id, node_id),
)
return [row["id"] for row in rows]
def get_all_node_links(name: str) -> dict[str, list[str]]:
"""Build the node adjacency map with one scan of the link table."""
rows = read_all_typed(
name,
"SELECT id, start_node_id, end_node_id FROM network.links ORDER BY id",
(),
)
result: dict[str, list[str]] = {}
for row in rows:
link_id = str(row["id"])
result.setdefault(str(row["start_node_id"]), []).append(link_id)
result.setdefault(str(row["end_node_id"]), []).append(link_id)
return result
def get_link_nodes(name: str, link_id: str) -> list[str]:
row = read(
name,
"""
SELECT start_node_id, end_node_id
FROM network.links WHERE id = %s
""",
(link_id,),
)
return [str(row["start_node_id"]), str(row["end_node_id"])]
def get_region_type(name: str, region_id: str) -> str:
row = read(
name,
"SELECT region_type FROM gis.regions WHERE id = %s",
(region_id,),
)
return row["region_type"]