diff --git a/api/__init__.py b/api/__init__.py index e222df7..e12dd91 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -19,8 +19,7 @@ from .database import pick_snapshot from .database import pick_operation, sync_with_server from .database import get_restore_operation, set_restore_operation, set_restore_operation_to_current, restore -from .batch_cmd import execute_batch_command -from .batch_cmds import execute_batch_commands +from .batch_cmds import execute_batch_commands, execute_batch_command from .s0_base import JUNCTION, RESERVOIR, TANK, PIPE, PUMP, VALVE, PATTERN, CURVE from .s0_base import is_node, is_junction, is_reservoir, is_tank diff --git a/api/batch_cmds.py b/api/batch_cmds.py index 4ee0688..6657524 100644 --- a/api/batch_cmds.py +++ b/api/batch_cmds.py @@ -1,5 +1,6 @@ +from typing import Any from .sections import * -from .database import API_ADD, API_UPDATE, API_DELETE, ChangeSet +from .database import API_ADD, API_UPDATE, API_DELETE, ChangeSet, write, read, read_all, get_current_operation from .s1_title import set_title from .s2_junctions import set_junction, add_junction, delete_junction from .s3_reservoirs import set_reservoir, add_reservoir, delete_reservoir @@ -290,7 +291,71 @@ def execute_batch_commands(name: str, cs: ChangeSet) -> ChangeSet: elif operation == API_DELETE: result.merge(execute_delete_command(name, ChangeSet(op))) except: - print(f'ERROR: Fail to execute {todo}!') - pass + print(f'ERROR: Fail to execute {todo}') + + return result + + +def execute_batch_command(name: str, cs: ChangeSet) -> ChangeSet: + write(name, 'delete from batch_operation where id > 0') + write(name, "update operation_table set option = 'batch_operation' where option = 'operation'") + + # for delete, generate cascade command + new_cs = ChangeSet() + for op in cs.operations: + if op['operation'] == API_DELETE: + new_cs.merge(del_cascade_cmd(name, ChangeSet(op))) + else: + new_cs.merge(ChangeSet(op)) + + result = ChangeSet() + + todo = {} + + try: + for op in new_cs.operations: + todo = op + operation = op['operation'] + if operation == API_ADD: + result.merge(execute_add_command(name, ChangeSet(op))) + elif operation == API_UPDATE: + result.merge(execute_update_command(name, ChangeSet(op))) + elif operation == API_DELETE: + result.merge(execute_delete_command(name, ChangeSet(op))) + except: + print(f'ERROR: Fail to execute {todo}!') + + count = read(name, 'select count(*) as count from batch_operation')['count'] + if count == 1: + write(name, 'delete from batch_operation where id > 0') + write(name, "update operation_table set option = 'operation' where option = 'batch_operation'") + return ChangeSet() + + redo_list: list[str] = [] + redo_cs_list: list[dict[str, Any]] = [] + redo_rows = read_all(name, 'select redo, redo_cs from batch_operation where id > 0 order by id asc') + for row in redo_rows: + redo_list.append(row['redo']) + redo_cs_list += eval(row['redo_cs']) + + undo_list: list[str] = [] + undo_cs_list: list[dict[str, Any]] = [] + undo_rows = read_all(name, 'select undo, undo_cs from batch_operation where id > 0 order by id desc') + for row in undo_rows: + undo_list.append(row['undo']) + undo_cs_list += eval(row['undo_cs']) + + redo = '\n'.join(redo_list).replace("'", "''") + redo_cs = str(redo_cs_list).replace("'", "''") + undo = '\n'.join(undo_list).replace("'", "''") + undo_cs = str(undo_cs_list).replace("'", "''") + + parent = get_current_operation(name) + write(name, f"insert into operation (id, redo, undo, parent, redo_cs, undo_cs) values (default, '{redo}', '{undo}', {parent}, '{redo_cs}', '{undo_cs}')") + current = read(name, 'select max(id) as id from operation')['id'] + write(name, f"update current_operation set id = {current}") + + write(name, 'delete from batch_operation where id > 0') + write(name, "update operation_table set option = 'operation' where option = 'batch_operation'") return result diff --git a/api/database.py b/api/database.py index a3e4788..6fd268b 100644 --- a/api/database.py +++ b/api/database.py @@ -113,6 +113,8 @@ def get_current_operation(name: str) -> int: def execute_command(name: str, command: DbChangeSet) -> ChangeSet: + op_table = read(name, "select * from operation_table")['option'] + write(name, command.redo_sql) parent = get_current_operation(name) @@ -120,10 +122,11 @@ def execute_command(name: str, command: DbChangeSet) -> ChangeSet: undo_sql = command.undo_sql.replace("'", "''") redo_cs_str = str(command.redo_cs).replace("'", "''") undo_cs_str = str(command.undo_cs).replace("'", "''") - write(name, f"insert into operation (id, redo, undo, parent, redo_cs, undo_cs) values (default, '{redo_sql}', '{undo_sql}', {parent}, '{redo_cs_str}', '{undo_cs_str}')") + write(name, f"insert into {op_table} (id, redo, undo, parent, redo_cs, undo_cs) values (default, '{redo_sql}', '{undo_sql}', {parent}, '{redo_cs_str}', '{undo_cs_str}')") - current = read(name, 'select max(id) as id from operation')['id'] - write(name, f"update current_operation set id = {current}") + if op_table == 'operation': + current = read(name, 'select max(id) as id from operation')['id'] + write(name, f"update current_operation set id = {current}") return ChangeSet.from_list(command.redo_cs) diff --git a/script/sql/create/operation.sql b/script/sql/create/operation.sql index dc4f220..3c7f952 100644 --- a/script/sql/create/operation.sql +++ b/script/sql/create/operation.sql @@ -30,3 +30,26 @@ create table restore_operation ); insert into restore_operation (id) values (0); + + +create table batch_operation +( + id bigserial primary key +, redo text not null +, undo text not null +, parent integer references operation(id) on delete cascade +, redo_child integer references operation(id) -- must update before delete +, redo_cs text not null +, undo_cs text not null +); + +insert into batch_operation (id, redo, undo, redo_cs, undo_cs) values (0, '', '', '', ''); + +create type operation_table_option as enum ('operation', 'batch_operation'); + +create table operation_table +( + option operation_table_option primary key +); + +insert into operation_table (option) values ('operation'); diff --git a/script/sql/drop/operation.sql b/script/sql/drop/operation.sql index 06a7765..ca063b2 100644 --- a/script/sql/drop/operation.sql +++ b/script/sql/drop/operation.sql @@ -1,3 +1,9 @@ +drop table if exists operation_table; + +drop type if exists operation_table_option; + +drop table if exists batch_operation; + drop table if exists restore_operation; drop table if exists snapshot_operation; diff --git a/test_tjnetwork.py b/test_tjnetwork.py index 6faee5c..a4df208 100644 --- a/test_tjnetwork.py +++ b/test_tjnetwork.py @@ -157,15 +157,7 @@ class TestApi: cs.add({'type': JUNCTION, 'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}) # fail cs = execute_batch_command(p, cs) - assert len(cs.operations) == 0 - - assert get_current_operation(p) == 0 - - cs = ChangeSet() - cs.add({'type': JUNCTION, 'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}) - cs.add({'type': JUNCTION, 'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}) - - cs = execute_batch_command(p, cs) + assert len(cs.operations) == 2 assert get_current_operation(p) == 1 @@ -399,6 +391,87 @@ class TestApi: self.leave(p) + def test_delete_nodes_then_restore_command(self): + p = 'test_delete_nodes_then_restore_commands' + read_inp(p, f'./inp/net3.inp', '2') + + open_project(p) + + nodes = get_nodes(p) + links = get_links(p) + + for _ in range(10): + random.shuffle(nodes) + + batch = ChangeSet() + for node in nodes: + if is_junction(p, node): + batch.delete({'type' : 'junction', 'id': node }) + if is_reservoir(p, node): + batch.delete({'type' : 'reservoir', 'id': node }) + if is_tank(p, node): + batch.delete({'type' : 'tank', 'id': node }) + execute_batch_command(p, batch) + + for node in nodes: + assert is_node(p, node) == False + for link in links: + assert is_link(p, link) == False + + assert get_nodes(p) == [] + assert get_links(p) == [] + + op = get_restore_operation(p) + pick_operation(p, op) + + for node in nodes: + assert is_node(p, node) + for link in links: + assert is_link(p, link) + + self.leave(p) + + + def test_delete_links_then_restore_command(self): + p = 'test_delete_links_then_restore_commands' + read_inp(p, f'./inp/net3.inp', '2') + + open_project(p) + + nodes = get_nodes(p) + links = get_links(p) + + for _ in range(10): + random.shuffle(links) + + batch = ChangeSet() + for link in links: + if is_pipe(p, link): + batch.delete({'type' : 'pipe', 'id': link }) + if is_pump(p, link): + batch.delete({'type' : 'pump', 'id': link }) + if is_valve(p, link): + batch.delete({'type' : 'valve', 'id': link }) + execute_batch_command(p, batch) + + for node in nodes: + assert is_node(p, node) + for link in links: + assert is_link(p, link) == False + + assert get_links(p) == [] + + op = get_restore_operation(p) + pick_operation(p, op) + + for node in nodes: + assert is_node(p, node) + for link in links: + assert is_link(p, link) + + self.leave(p) + + def test_delete_nodes_links_then_restore_v2(self): p = 'test_delete_nodes_links_then_restore_v2' read_inp(p, f'./inp/net3.inp', '2')