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
+1
View File
@@ -0,0 +1 @@
"""Model command rewriting, cascade handling, and execution."""
+65
View File
@@ -0,0 +1,65 @@
"""Convenience entry points for cascade deletes and option synchronization."""
from ..core.database import API_DELETE, API_UPDATE, ChangeSet
from .executor import execute_batch_command
def _execute(
name: str,
change_set: ChangeSet,
*,
operation: str,
element_type: str,
) -> ChangeSet:
change_set.operations[0].update(
{"operation": operation, "type": element_type}
)
return execute_batch_command(name, change_set)
def delete_junction_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
return _execute(
name, change_set, operation=API_DELETE, element_type="junction"
)
def delete_reservoir_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
return _execute(
name, change_set, operation=API_DELETE, element_type="reservoir"
)
def delete_tank_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
return _execute(name, change_set, operation=API_DELETE, element_type="tank")
def delete_pipe_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
return _execute(name, change_set, operation=API_DELETE, element_type="pipe")
def delete_pump_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
return _execute(name, change_set, operation=API_DELETE, element_type="pump")
def delete_valve_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
return _execute(name, change_set, operation=API_DELETE, element_type="valve")
def delete_pattern_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
return _execute(
name, change_set, operation=API_DELETE, element_type="pattern"
)
def delete_curve_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
return _execute(name, change_set, operation=API_DELETE, element_type="curve")
def set_option_ex(name: str, change_set: ChangeSet) -> ChangeSet:
return _execute(name, change_set, operation=API_UPDATE, element_type="option")
def set_option_v3_ex(name: str, change_set: ChangeSet) -> ChangeSet:
return _execute(
name, change_set, operation=API_UPDATE, element_type="option_v3"
)
+244
View File
@@ -0,0 +1,244 @@
from collections.abc import Callable
from ..core.database import API_DELETE, API_UPDATE, ChangeSet, g_delete_prefix, try_read
from ..model.elements import get_node_links, is_pipe, is_pump, is_valve
from ..model.reservoirs import unset_reservoir_by_pattern
from ..model.tanks import unset_tank_by_curve
from ..model.pumps import unset_pump_by_curve, unset_pump_by_pattern
from ..model.tags import delete_tag_by_node, delete_tag_by_link
from ..model.demands import delete_demand_by_junction, unset_demand_by_pattern
from ..model.status import delete_status_by_link
from ..model.energy import delete_pump_energy_by_pump, unset_pump_energy_by_pattern, unset_pump_energy_by_curve
from ..model.emitters import delete_emitter_by_junction
from ..model.quality import delete_quality_by_node
from ..model.sources import delete_source_by_node, unset_source_by_pattern
from ..model.reactions import delete_pipe_reaction_by_pipe, delete_tank_reaction_by_tank
from ..model.mixing import delete_mixing_by_tank
from ..gis.vertices import delete_vertex_by_link
from ..gis.labels import unset_label_by_node
from ..model.options import generate_v2, generate_v3
def expand_junction_delete(name: str, cs: ChangeSet) -> ChangeSet:
result = ChangeSet()
id = cs.operations[0]['id']
row = try_read(name, "select 1 from network.junctions where node_id = %s", (id,))
if row == None:
return result
links = get_node_links(name, id)
for link in links:
if is_pipe(name, link):
result.merge(expand_pipe_delete(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': link})))
if is_pump(name, link):
result.merge(expand_pump_delete(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': link})))
if is_valve(name, link):
result.merge(expand_valve_delete(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': link})))
result.merge(delete_tag_by_node(name, id))
result.merge(delete_demand_by_junction(name, id))
result.merge(delete_emitter_by_junction(name, id))
result.merge(delete_quality_by_node(name, id))
result.merge(delete_source_by_node(name, id))
result.merge(unset_label_by_node(name, id))
result.merge(cs)
return result
def expand_reservoir_delete(name: str, cs: ChangeSet) -> ChangeSet:
result = ChangeSet()
id = cs.operations[0]['id']
row = try_read(name, "select 1 from network.reservoirs where node_id = %s", (id,))
if row == None:
return result
links = get_node_links(name, id)
for link in links:
if is_pipe(name, link):
result.merge(expand_pipe_delete(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': link})))
if is_pump(name, link):
result.merge(expand_pump_delete(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': link})))
if is_valve(name, link):
result.merge(expand_valve_delete(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': link})))
result.merge(delete_tag_by_node(name, id))
result.merge(delete_quality_by_node(name, id))
result.merge(delete_source_by_node(name, id))
result.merge(unset_label_by_node(name, id))
result.merge(cs)
return result
def expand_tank_delete(name: str, cs: ChangeSet) -> ChangeSet:
result = ChangeSet()
id = cs.operations[0]['id']
row = try_read(name, "select 1 from network.tanks where node_id = %s", (id,))
if row == None:
return result
links = get_node_links(name, id)
for link in links:
if is_pipe(name, link):
result.merge(expand_pipe_delete(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': link})))
if is_pump(name, link):
result.merge(expand_pump_delete(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': link})))
if is_valve(name, link):
result.merge(expand_valve_delete(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': link})))
result.merge(delete_tag_by_node(name, id))
result.merge(delete_quality_by_node(name, id))
result.merge(delete_source_by_node(name, id))
result.merge(delete_tank_reaction_by_tank(name, id))
result.merge(delete_mixing_by_tank(name, id))
result.merge(unset_label_by_node(name, id))
result.merge(cs)
return result
def expand_pipe_delete(name: str, cs: ChangeSet) -> ChangeSet:
result = ChangeSet()
id = cs.operations[0]['id']
row = try_read(name, "select 1 from network.pipes where link_id = %s", (id,))
if row == None:
return result
result.merge(delete_tag_by_link(name, id))
result.merge(delete_status_by_link(name, id))
result.merge(delete_pipe_reaction_by_pipe(name, id))
result.merge(delete_vertex_by_link(name, id))
result.merge(cs)
return result
def expand_pump_delete(name: str, cs: ChangeSet) -> ChangeSet:
result = ChangeSet()
id = cs.operations[0]['id']
row = try_read(name, "select 1 from network.pumps where link_id = %s", (id,))
if row == None:
return result
result.merge(delete_tag_by_link(name, id))
result.merge(delete_status_by_link(name, id))
result.merge(delete_pump_energy_by_pump(name, id))
result.merge(delete_vertex_by_link(name, id))
result.merge(cs)
return result
def expand_valve_delete(name: str, cs: ChangeSet) -> ChangeSet:
result = ChangeSet()
id = cs.operations[0]['id']
row = try_read(name, "select 1 from network.valves where link_id = %s", (id,))
if row == None:
return result
result.merge(delete_tag_by_link(name, id))
result.merge(delete_status_by_link(name, id))
result.merge(delete_vertex_by_link(name, id))
result.merge(cs)
return result
def expand_pattern_delete(name: str, cs: ChangeSet) -> ChangeSet:
result = ChangeSet()
id = cs.operations[0]['id']
row = try_read(name, "select 1 from network.patterns where id = %s", (id,))
if row == None:
return result
result.merge(unset_reservoir_by_pattern(name, id))
result.merge(unset_pump_by_pattern(name, id))
result.merge(unset_demand_by_pattern(name, id))
result.merge(unset_pump_energy_by_pattern(name, id))
result.merge(unset_source_by_pattern(name, id))
result.merge(cs)
return result
def expand_curve_delete(name: str, cs: ChangeSet) -> ChangeSet:
result = ChangeSet()
id = cs.operations[0]['id']
row = try_read(name, "select 1 from network.curves where id = %s", (id,))
if row == None:
return result
result.merge(unset_tank_by_curve(name, id))
result.merge(unset_pump_by_curve(name, id))
result.merge(unset_pump_energy_by_curve(name, id))
result.merge(cs)
return result
def expand_legacy_options_update(cs: ChangeSet) -> ChangeSet:
cs.operations[0]['operation'] = API_UPDATE
cs.operations[0]['type'] = 'option'
new_cs = cs
new_cs.merge(generate_v3(cs))
return new_cs
def expand_v3_options_update(cs: ChangeSet) -> ChangeSet:
cs.operations[0]['operation'] = API_UPDATE
cs.operations[0]['type'] = 'option_v3'
new_cs = cs
new_cs.merge(generate_v2(cs))
return new_cs
def expand_command(name: str, cs: ChangeSet) -> ChangeSet:
op = cs.operations[0]
operation = op['operation']
element_type = op['type']
if operation == API_DELETE:
handler = _DELETE_REWRITERS.get(element_type)
if handler:
return handler(name, cs)
elif operation == API_UPDATE:
handler = _UPDATE_REWRITERS.get(element_type)
if handler:
return handler(cs)
return cs
DeleteRewriter = Callable[[str, ChangeSet], ChangeSet]
UpdateRewriter = Callable[[ChangeSet], ChangeSet]
_DELETE_REWRITERS: dict[str, DeleteRewriter] = {
"junction": expand_junction_delete,
"reservoir": expand_reservoir_delete,
"tank": expand_tank_delete,
"pipe": expand_pipe_delete,
"pump": expand_pump_delete,
"valve": expand_valve_delete,
"pattern": expand_pattern_delete,
"curve": expand_curve_delete,
}
_UPDATE_REWRITERS: dict[str, UpdateRewriter] = {
"option": expand_legacy_options_update,
"option_v3": expand_v3_options_update,
}
+155
View File
@@ -0,0 +1,155 @@
"""Transactional command dispatch for WNDB model mutations."""
from collections.abc import Callable
from ..core.connection import project_transaction
from ..core.database import (
API_ADD,
API_DELETE,
API_UPDATE,
ChangeSet,
refresh_materialized_views,
)
from ..gis.backdrop import set_backdrop
from ..gis.labels import add_label, delete_label, set_label
from ..gis.regions import add_region, delete_region, set_region
from ..gis.vertices import add_vertex, delete_vertex, set_vertex
from ..model.controls import set_control
from ..model.curves import add_curve, delete_curve, set_curve
from ..model.demands import set_demand
from ..model.emitters import set_emitter
from ..model.energy import set_energy, set_pump_energy
from ..model.junctions import add_junction, delete_junction, set_junction
from ..model.mixing import add_mixing, delete_mixing, set_mixing
from ..model.options import set_option, set_option_v3
from ..model.patterns import add_pattern, delete_pattern, set_pattern
from ..model.pipes import add_pipe, delete_pipe, set_pipe
from ..model.pumps import add_pump, delete_pump, set_pump
from ..model.quality import set_quality
from ..model.reactions import (
set_pipe_reaction,
set_reaction,
set_tank_reaction,
)
from ..model.reservoirs import add_reservoir, delete_reservoir, set_reservoir
from ..model.rules import set_rule
from ..model.sources import add_source, delete_source, set_source
from ..model.status import set_status
from ..model.tags import set_tag
from ..model.tanks import add_tank, delete_tank, set_tank
from ..model.times import set_time
from ..model.title import set_title
from ..model.valves import add_valve, delete_valve, set_valve
from .cascade import expand_command
CommandHandler = Callable[[str, ChangeSet], ChangeSet]
_ADD_HANDLERS: dict[str, CommandHandler] = {
"junction": add_junction,
"reservoir": add_reservoir,
"tank": add_tank,
"pipe": add_pipe,
"pump": add_pump,
"valve": add_valve,
"pattern": add_pattern,
"curve": add_curve,
"source": add_source,
"mixing": add_mixing,
"vertex": add_vertex,
"label": add_label,
"region": add_region,
}
_UPDATE_HANDLERS: dict[str, CommandHandler] = {
"title": set_title,
"junction": set_junction,
"reservoir": set_reservoir,
"tank": set_tank,
"pipe": set_pipe,
"pump": set_pump,
"valve": set_valve,
"tag": set_tag,
"demand": set_demand,
"status": set_status,
"pattern": set_pattern,
"curve": set_curve,
"control": set_control,
"rule": set_rule,
"energy": set_energy,
"pump_energy": set_pump_energy,
"emitter": set_emitter,
"quality": set_quality,
"source": set_source,
"reaction": set_reaction,
"pipe_reaction": set_pipe_reaction,
"tank_reaction": set_tank_reaction,
"mixing": set_mixing,
"time": set_time,
"option": set_option,
"option_v3": set_option_v3,
"vertex": set_vertex,
"label": set_label,
"backdrop": set_backdrop,
"region": set_region,
}
_DELETE_HANDLERS: dict[str, CommandHandler] = {
"junction": delete_junction,
"reservoir": delete_reservoir,
"tank": delete_tank,
"pipe": delete_pipe,
"pump": delete_pump,
"valve": delete_valve,
"pattern": delete_pattern,
"curve": delete_curve,
"source": delete_source,
"mixing": delete_mixing,
"vertex": delete_vertex,
"label": delete_label,
"region": delete_region,
}
def _dispatch(
handlers: dict[str, CommandHandler], name: str, change_set: ChangeSet
) -> ChangeSet:
element_type = change_set.operations[0]["type"]
handler = handlers.get(element_type)
return handler(name, change_set) if handler else ChangeSet()
def _execute_add_command(name: str, change_set: ChangeSet) -> ChangeSet:
return _dispatch(_ADD_HANDLERS, name, change_set)
def _execute_update_command(name: str, change_set: ChangeSet) -> ChangeSet:
return _dispatch(_UPDATE_HANDLERS, name, change_set)
def _execute_delete_command(name: str, change_set: ChangeSet) -> ChangeSet:
return _dispatch(_DELETE_HANDLERS, name, change_set)
def execute_batch_commands(name: str, change_set: ChangeSet) -> ChangeSet:
with project_transaction(name):
rewritten = ChangeSet()
for operation in change_set.operations:
rewritten.merge(expand_command(name, ChangeSet(operation)))
result = ChangeSet()
for operation in rewritten.operations:
operation_type = operation["operation"]
if operation_type == API_ADD:
result.merge(_execute_add_command(name, ChangeSet(operation)))
elif operation_type == API_UPDATE:
result.merge(_execute_update_command(name, ChangeSet(operation)))
elif operation_type == API_DELETE:
result.merge(_execute_delete_command(name, ChangeSet(operation)))
if rewritten.operations:
refresh_materialized_views(name)
return result
def execute_batch_command(name: str, change_set: ChangeSet) -> ChangeSet:
return execute_batch_commands(name, change_set)