refactor(db)!: finalize pooled WNDB v2 migration

This commit is contained in:
2026-08-27 17:26:22 +08:00
parent fa188af0b1
commit b74799a39d
105 changed files with 4988 additions and 5565 deletions
+16 -54
View File
@@ -1,10 +1,5 @@
from psycopg.rows import dict_row
from ..core.database import read_all, sql_literal, try_read
from ..core.connection import project_connection
from ..core.database import read_all, sql_literal, try_read, write
from ..core.connection import project_connection
from ..model.elements import get_link_nodes
from psycopg.rows import dict_row
def sql_update_coord(node: str, x: float, y: float) -> str:
geom = f"st_setsrid(st_makepoint({sql_literal(x)}, {sql_literal(y)}), 900914)"
@@ -21,8 +16,8 @@ def sql_delete_coord(node: str) -> str:
def from_postgis_point(coord: str) -> dict[str, float]:
xy = coord.lower().removeprefix('point(').removesuffix(')').split(' ')
return { 'x': float(xy[0]), 'y': float(xy[1]) }
xy = coord.lower().removeprefix("point(").removesuffix(")").split(" ")
return {"x": float(xy[0]), "y": float(xy[1])}
def get_node_coord(name: str, node: str) -> dict[str, float]:
@@ -31,51 +26,15 @@ def get_node_coord(name: str, node: str) -> dict[str, float]:
"select st_astext(geom) as coord_geom from gis.node_geometries where node_id = %s",
(node,),
)
if row == None:
write(name, sql_insert_coord(node, 0.0, 0.0))
return {'x': 0.0, 'y': 0.0}
return from_postgis_point(row['coord_geom'])
# DingZQ 2025-01-03, get nodes in extent
# return node id list
# node_id:junction:x:y
def get_nodes_in_extent(name: str, x1: float, y1: float, x2: float, y2: float) -> list[str]:
nodes = []
objs = read_all(name, 'select node_id, st_astext(geom) as coord_geom from gis.node_geometries')
for obj in objs:
node_id = obj['node_id']
coord = from_postgis_point(obj['coord_geom'])
x = coord['x']
y = coord['y']
if x1 <= x <= x2 and y1 <= y <= y2:
nodes.append(f"{node_id}:junction:{x}:{y}")
return nodes
# DingZQ 2025-01-03, get links in extent
# return link id list
# link_id:pipe:node_id1:node_id2
def get_links_in_extent(name: str, x1: float, y1: float, x2: float, y2: float) -> list[str]:
node_ids = set([s.split(':')[0] for s in get_nodes_in_extent(name, x1, y1, x2, y2)])
all_link_ids = []
with project_connection(name) as conn:
with conn.cursor(row_factory=dict_row) as cur:
cur.execute("select link_id from network.pipes")
for record in cur:
all_link_ids.append(record['link_id'])
links = []
for link_id in all_link_ids:
nodes = get_link_nodes(name, link_id)
if nodes[0] in node_ids and nodes[1] in node_ids:
links.append(f"{link_id}:pipe:{nodes[0]}:{nodes[1]}")
return links
if row is None:
return {"x": 0.0, "y": 0.0}
return from_postgis_point(row["coord_geom"])
def node_has_coord(name: str, node: str) -> bool:
return try_read(
name, "select node_id from gis.node_geometries where node_id = %s", (node,)
) != None
) is not None
#--------------------------------------------------------------
@@ -94,11 +53,14 @@ def inp_in_coord(line: str) -> str:
def inp_out_coord(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select node_id, st_astext(geom) as coord_geom from gis.node_geometries')
objs = read_all(
name,
"select node_id, st_astext(geom) as coord_geom from gis.node_geometries",
)
for obj in objs:
node = obj['node_id']
coord = from_postgis_point(obj['coord_geom'])
x = coord['x']
y = coord['y']
lines.append(f'{node} {x} {y}')
node = obj["node_id"]
coord = from_postgis_point(obj["coord_geom"])
x = coord["x"]
y = coord["y"]
lines.append(f"{node} {x} {y}")
return lines
+191
View File
@@ -0,0 +1,191 @@
"""Read-only projections backed by the GIS materialized-view query layer."""
from typing import Any
from ..core.database import read, read_all
def _node_coord_rows(
rows: list[Any],
) -> dict[str, dict[str, Any]]:
return {
str(row["id"]): {
"x": float(row["x"]),
"y": float(row["y"]),
"type": str(row["node_type"]),
}
for row in rows
}
def get_network_node_coords(name: str) -> dict[str, dict[str, Any]]:
"""Return every publishable node with one view-backed query."""
rows = read_all(
name,
"""
SELECT id, x, y, node_type
FROM gis.network_nodes
ORDER BY id
""",
)
return _node_coord_rows(rows)
def get_major_node_coords(
name: str, diameter: int
) -> dict[str, dict[str, Any]]:
"""Return endpoints of pipes above the requested diameter."""
rows = read_all(
name,
"""
SELECT n.id, n.x, n.y, n.node_type
FROM gis.pipes AS p
CROSS JOIN LATERAL (
VALUES (p.start_node_id), (p.end_node_id)
) AS endpoint(id)
JOIN gis.network_nodes AS n ON n.id = endpoint.id
WHERE p.diameter > %s
GROUP BY n.id, n.x, n.y, n.node_type
ORDER BY n.id
""",
(diameter,),
)
return _node_coord_rows(rows)
def get_network_link_nodes(name: str) -> list[str]:
"""Return every publishable link in the established API wire format."""
rows = read_all(
name,
"""
SELECT id, link_type, start_node_id, end_node_id
FROM gis.network_links
ORDER BY id
""",
)
return [
f"{row['id']}:{row['link_type']}:{row['start_node_id']}:{row['end_node_id']}"
for row in rows
]
def get_major_pipe_nodes(name: str, diameter: int) -> list[str]:
"""Return large pipes in the established API wire format."""
rows = read_all(
name,
"""
SELECT id, start_node_id, end_node_id
FROM gis.pipes
WHERE diameter > %s
ORDER BY id
""",
(diameter,),
)
return [
f"{row['id']}:pipe:{row['start_node_id']}:{row['end_node_id']}"
for row in rows
]
def get_topology_rows(
name: str, node_ids: list[str]
) -> tuple[list[Any], list[Any]]:
"""Load selected nodes and their internal links with two batch queries."""
if not node_ids:
return [], []
nodes = read_all(
name,
"""
SELECT n.id,
ST_X(g.geom) AS x,
ST_Y(g.geom) AS y,
n.node_type::text AS node_type
FROM network.nodes AS n
JOIN gis.node_geometries AS g ON g.node_id = n.id
WHERE n.id = ANY(%s)
ORDER BY n.id
""",
(node_ids,),
)
links = read_all(
name,
"""
SELECT l.id,
l.start_node_id,
l.end_node_id,
COALESCE(p.length, 0.0) AS length
FROM network.links AS l
LEFT JOIN network.pipes AS p ON p.link_id = l.id
WHERE l.start_node_id = ANY(%s)
AND l.end_node_id = ANY(%s)
ORDER BY l.id
""",
(node_ids, node_ids),
)
return nodes, links
def get_boundary_link_ids(name: str, node_ids: list[str]) -> list[str]:
"""Return links with exactly one endpoint inside the supplied node set."""
if not node_ids:
return []
rows = read_all(
name,
"""
SELECT id
FROM network.links
WHERE (start_node_id = ANY(%s)) <> (end_node_id = ANY(%s))
ORDER BY id
""",
(node_ids, node_ids),
)
return [str(row["id"]) for row in rows]
def get_junction_demands(
name: str, node_ids: list[str]
) -> dict[str, list[dict[str, Any]]]:
"""Read authoritative demands for selected junctions from model tables."""
if not node_ids:
return {}
rows = read_all(
name,
"""
SELECT junction_id,
sequence_no,
base_demand,
pattern_id,
category
FROM network.demands
WHERE junction_id = ANY(%s)
ORDER BY junction_id, sequence_no
""",
(node_ids,),
)
result: dict[str, list[dict[str, Any]]] = {}
for row in rows:
result.setdefault(str(row["junction_id"]), []).append(
{
"demand": float(row["base_demand"]),
"pattern": row["pattern_id"],
"category": row["category"],
}
)
return result
def sum_junction_base_demand(name: str, node_ids: list[str]) -> float:
"""Sum selected junction demand from the authoritative model table."""
if not node_ids:
return 0.0
row = read(
name,
"""
SELECT COALESCE(SUM(base_demand), 0.0) AS total_base_demand
FROM network.demands
WHERE junction_id = ANY(%s)
""",
(node_ids,),
)
return float(row["total_base_demand"])
+33 -47
View File
@@ -2,10 +2,8 @@ import platform
import math
from typing import Any
import pyclipper
from ..model.elements import get_node_links, get_link_nodes, is_pipe
from ..model.pipes import get_pipe
from ..core.database import read, try_read, read_all
from .coordinates import node_has_coord, get_node_coord
from .network_views import get_boundary_link_ids, get_topology_rows
def from_postgis_polygon(polygon: str) -> list[tuple[float, float]]:
@@ -42,21 +40,7 @@ def get_nodes_in_boundary(name: str, boundary: list[tuple[float, float]]) -> lis
def _get_links_on_boundary(name: str, nodes: list[str]) -> list[str]:
links: list[str] = []
for node in nodes:
node_links = get_node_links(name, node)
for link in node_links:
if link in links:
continue
link_nodes = get_link_nodes(name, link)
if link_nodes[0] in nodes and link_nodes[1] not in nodes:
links.append(link)
elif link_nodes[0] not in nodes and link_nodes[1] in nodes:
links.append(link)
return links
return get_boundary_link_ids(name, nodes)
def get_nodes_in_region(name: str, region_id: str) -> list[str]:
@@ -128,37 +112,39 @@ def _angle_of_node_link(node: str, link: str, nodes, links) -> float:
class Topology:
def __init__(self, db: str, nodes: list[str]) -> None:
self._nodes: dict[str, Any] = {}
self._max_x_node = ''
self._node_list: list[str] = []
for node in nodes:
if not node_has_coord(db, node):
continue
if get_node_links(db, node) == 0:
continue
self._nodes[node] = get_node_coord(db, node) | { 'links': [] }
self._node_list.append(node)
if self._max_x_node == '' or self._nodes[node]['x'] > self._nodes[self._max_x_node]['x']:
self._max_x_node = node
node_rows, link_rows = get_topology_rows(db, nodes)
self._nodes: dict[str, Any] = {
str(row["id"]): {
"x": float(row["x"]),
"y": float(row["y"]),
"type": str(row["node_type"]),
"links": [],
}
for row in node_rows
}
self._node_list = list(self._nodes)
self._max_x_node = max(
self._nodes,
key=lambda node_id: self._nodes[node_id]["x"],
default="",
)
self._links: dict[str, Any] = {}
self._link_list: list[str] = []
for node in self._nodes:
for link in get_node_links(db, node):
candidate = True
link_nodes = get_link_nodes(db, link)
for link_node in link_nodes:
if link_node not in self._nodes:
candidate = False
break
if candidate:
length = get_pipe(db, link)['length'] if is_pipe(db, link) else 0.0
self._links[link] = { 'node1' : link_nodes[0], 'node2' : link_nodes[1], 'length' : length }
self._link_list.append(link)
if link not in self._nodes[link_nodes[0]]['links']:
self._nodes[link_nodes[0]]['links'].append(link)
if link not in self._nodes[link_nodes[1]]['links']:
self._nodes[link_nodes[1]]['links'].append(link)
for row in link_rows:
link_id = str(row["id"])
node1 = str(row["start_node_id"])
node2 = str(row["end_node_id"])
if node1 not in self._nodes or node2 not in self._nodes:
continue
self._links[link_id] = {
"node1": node1,
"node2": node2,
"length": float(row["length"]),
}
self._nodes[node1]["links"].append(link_id)
self._nodes[node2]["links"].append(link_id)
self._link_list = list(self._links)
def nodes(self):
return self._nodes