from psycopg.rows import dict_row, Row from .connection import g_conn_dict as conn from .utility import * from .change_set import * API_ADD = 'add' API_DELETE = 'delete' API_UPDATE = 'update' def _remove_operation(name: str, id: int) -> None: row = read(name, f'select * from operation where parent = {id}') if row != None: raise Exception('Disallow to remove parent operation !') sql += f'delete from snapshot_operation where id = {id};' sql += f'delete from operation where id = {id}' write(name, sql) def _get_parents(name: str, id: int) -> list[int]: ids = [id] while ids[-1] != 0: row = read(name, f'select parent from operation where id = {ids[-1]}') ids.append(int(row['parent'])) return ids def get_current_operation(name: str) -> int: row = read(name, f'select id from current_operation') return int(row['id']) def _update_current_operation(name: str, old_id: int, id: int) -> None: return write(name, f'update current_operation set id = {id} where id = {old_id}') def _add_redo_undo(name: str, redo: str, undo: str, api_id: str, api_op: str, api_object_type: str, api_object_id: str, api_object_properties: list[str]) -> int: parent = get_current_operation(name) ps = [] for p in api_object_properties: ps.append(f'"{p}"') if len(ps) > 0: ps = ','.join(ps) ps = '{' + ps + '}' sql = f"insert into operation (id, redo, undo, parent, api_id, api_op, api_object_type, api_object_id, api_object_properties) values (default, '{redo}', '{undo}', {parent}, '{api_id}', '{api_op}', '{api_object_type}', '{api_object_id}', '{ps}')" else: sql = f"insert into operation (id, redo, undo, parent, api_id, api_op, api_object_type, api_object_id) values (default, '{redo}', '{undo}', {parent}, '{api_id}', '{api_op}', '{api_object_type}', '{api_object_id}')" write(name, sql) return int(read(name, 'select max(id) from operation')['max']) def _query_operation(name: str, id: str) -> dict[str, str]: return read(name, f'select * from operation where id = {id}') def _query_redo_child(name: str, id: str) -> str: row = read(name, f'select redo_child from operation where id = {id}') return row['redo_child'] def _set_redo_child(name: str, id: int, child: int | str) -> None: return write(name, f'update operation set redo_child = {child} where id = {id}') def add_operation(name: str, redo: str, undo: str, api_id: str, api_op: str, api_object_type: str, api_object_id: str, api_object_properties: list[str] = []) -> None: curr = _add_redo_undo(name, redo, undo, api_id, api_op, api_object_type, api_object_id, api_object_properties) old = get_current_operation(name) _update_current_operation(name, old, curr) def _reverser_op(op: str) -> str: if op == API_ADD: return API_DELETE elif op == API_DELETE: return API_ADD else: return op def _get_change_set(row: dict[str, str], undo: bool) -> ChangeSet: op = row['api_op'] if undo: op = _reverser_op(op) type = row['api_object_type'] id = row['api_object_id'] change = ChangeSet() if op == API_ADD: change.add(type, id) elif op == API_DELETE: change.delete(type, id) elif op == API_UPDATE: ps = row['api_object_properties'].removeprefix('{').removesuffix('}').split(',') change.update(type, id, ps) return change def execute_undo(name: str, discard: bool) -> ChangeSet: curr = get_current_operation(name) row = _query_operation(name, curr) undo = row['undo'] if undo == '': print("nothing to undo!") return change = _get_change_set(row, True) parent = int(row['parent']) _set_redo_child(name, parent, 'null' if discard else curr) write(name, undo) _update_current_operation(name, curr, parent) if discard: _remove_operation(name, curr) return change def execute_redo(name: str) -> ChangeSet: curr = get_current_operation(name) redoChild = _query_redo_child(name, curr) if redoChild == None: print("nothing to redo!") return child = int(redoChild) row = _query_operation(name, child) redo = row['redo'] change = _get_change_set(row, False) write(name, redo) _update_current_operation(name, curr, child) return change def _get_operation_by_tag(name: str, tag: str) -> int | None: row = read(name, f"select id from snapshot_operation where tag = '{tag}'") return int(row['id']) if row != None else None def have_snapshot(name: str, tag: str) -> bool: return _get_operation_by_tag(name, tag) != None def take_snapshot(name: str, tag: str) -> int | None: if tag == None or tag == '': print('Non empty tag is expected!') return None curr = get_current_operation(name) write(name, f"insert into snapshot_operation (id, tag) values ({curr}, '{tag}')") return curr def pick_snapshot(name: str, tag: str, discard: bool) -> ChangeSet: if tag == None or tag == '': print('Non empty tag is expected!') return ChangeSet() target = _get_operation_by_tag(name, tag) if target == None: print('No such snapshot!') return ChangeSet() curr = get_current_operation(name) curr_parents = _get_parents(name, curr) target_parents = _get_parents(name, target) change = ChangeSet() if target in curr_parents: for _ in range(curr_parents.index(target)): change.append(execute_undo(name, discard)) elif curr in target_parents: target_parents.reverse() curr_index = target_parents.index(curr) for i in range(curr_index, len(target_parents) - 1): write(name, f"update operation set redo_child = '{target_parents[i + 1]}' where id = '{target_parents[i]}'") change.append(execute_redo(name)) else: ancestor_index = -1 while curr_parents[ancestor_index] == target_parents[ancestor_index]: ancestor_index -= 1 ancestor = curr_parents[ancestor_index + 1] for _ in range(curr_parents.index(ancestor)): change.append(execute_undo(name, discard)) target_parents.reverse() curr_index = target_parents.index(ancestor) for i in range(curr_index, len(target_parents) - 1): write(name, f"update operation set redo_child = '{target_parents[i + 1]}' where id = '{target_parents[i]}'") change.append(execute_redo(name)) return change.compress() def get_change_set(name: str, operation: int, undo: bool) -> ChangeSet: row = read(name, f'select api_id, api_op, api_object_type, api_object_id, api_object_properties from operation where id = {operation}') return _get_change_set(row, undo) def get_current_change_set(name: str) -> ChangeSet: return get_change_set(name, get_current_operation(name), False) def sync_with_server(name: str, operation: int) -> ChangeSet: fr = operation to = get_current_operation(name) fr_parents = _get_parents(name, fr) to_parents = _get_parents(name, to) change = ChangeSet() if fr in to_parents: index = to_parents.index(fr) - 1 while index >= 0: change.append(get_change_set(name, to_parents[index], False)) index -= 1 elif to in fr_parents: index = 0 while index <= fr_parents.index(to) - 1: change.append(get_change_set(name, fr_parents[index], True)) index += 1 else: ancestor_index = -1 while fr_parents[ancestor_index] == to_parents[ancestor_index]: ancestor_index -= 1 ancestor = fr_parents[ancestor_index + 1] index = 0 while index <= fr_parents.index(ancestor) - 1: change.append(get_change_set(name, fr_parents[index], True)) index += 1 index = to_parents.index(ancestor) - 1 while index >= 0: change.append(get_change_set(name, to_parents[index], False)) index -= 1 return change.compress()