Big change to operation!

This commit is contained in:
wqy
2022-09-24 23:00:55 +08:00
parent 46df1beedb
commit 11e30cc49f
6 changed files with 286 additions and 256 deletions

View File

@@ -1,111 +1,130 @@
from psycopg.rows import dict_row, Row
from .connection import g_conn_dict as conn
from .utility import *
from .change_set import *
def _get_current_transaction(name: str) -> Row | None:
with conn[name].cursor(row_factory=dict_row) as cur:
cur.execute(f"select * from transaction_operation")
return cur.fetchone()
def _get_current_transaction_id(name: str) -> int:
row = _get_current_transaction(name)
return int(row['id'])
API_ADD = 'add'
API_DELETE = 'delete'
API_UPDATE = 'update'
def _remove_transaction(name: str) -> None:
with conn[name].cursor() as cur:
cur.execute(f"delete from transaction_operation")
def _remove_operation(name: str, id: int) -> None:
with conn[name].cursor(row_factory=dict_row) as cur:
# can not be >= to cascade delete since there is a tree !
cur.execute(f"delete from transaction_operation where id = {id}") # this should not happen
cur.execute(f"delete from snapshot_operation where id = {id}") # this may happen
cur.execute(f"delete from operation where id = {id}")
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]
with conn[name].cursor(row_factory=dict_row) as cur:
while ids[-1] != 0:
cur.execute(f"select parent from operation where id = {ids[-1]}")
ids.append(int(cur.fetchone()['parent']))
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:
with conn[name].cursor(row_factory=dict_row) as cur:
cur.execute(f"select id from current_operation")
return int(cur.fetchone()['id'])
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:
with conn[name].cursor() as cur:
cur.execute(f"update current_operation set id = {id} where id = {old_id}")
return write(name, f'update current_operation set id = {id} where id = {old_id}')
def _add_redo_undo(name: str, redo: str, undo: str) -> int:
with conn[name].cursor(row_factory=dict_row) as cur:
parent = get_current_operation(name)
cur.execute(f"insert into operation (id, redo, undo, parent) values (default, '{redo}', '{undo}', {parent})")
cur.execute("select max(id) from operation")
return int(cur.fetchone()['max'])
# execute curr undo
def _query_undo(name: str, id: str) -> dict[str, str]:
with conn[name].cursor(row_factory=dict_row) as cur:
cur.execute(f"select undo, parent from operation where id = {id}")
return cur.fetchone()
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}')
# execute next redo
def _query_redo_child(name: str, id: str) -> str:
with conn[name].cursor(row_factory=dict_row) as cur:
cur.execute(f"select redo_child from operation where id = {id}")
return cur.fetchone()['redo_child']
row = read(name, f'select redo_child from operation where id = {id}')
return row['redo_child']
def _query_redo(name: str, id: str) -> dict[str, str]:
with conn[name].cursor(row_factory=dict_row) as cur:
cur.execute(f"select redo from operation where id = {id}")
return cur.fetchone()['redo']
def _set_redo_child(name: str, id: int, child: int | str) -> None:
with conn[name].cursor() as cur:
cur.execute(f"update operation set redo_child = {child} where id = {id}")
return write(name, f'update operation set redo_child = {child} where id = {id}')
def _execute(name: str, sql: str) -> None:
with conn[name].cursor() as cur:
sql = sql.replace("\"", "\'")
cur.execute(sql)
def add_operation(name: str, redo: str, undo: str) -> None:
curr = _add_redo_undo(name, redo, undo)
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 execute_undo(name: str, discard: bool = False) -> None:
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)
# transaction control
if have_transaction(name):
tran = _get_current_transaction(name)
if tran != None and int(tran['id']) >= 0:
if bool(tran['strict']): # strict mode disallow undo
print("Do not allow to undo in strict transaction mode!")
return
elif int(tran['id']) >= curr: # normal mode disallow undo start point, and there is foreign key constraint
print("Do not allow to undo transaction start point!")
return
row = _query_undo(name, curr)
row = _query_operation(name, curr)
undo = row['undo']
if undo == '':
print("nothing to undo!")
return
parent = int(row['parent'])
_set_redo_child(name, parent, 'NULL' if discard else curr)
change = _get_change_set(row, True)
_execute(name, undo)
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)
def execute_redo(name: str) -> None:
return change
def execute_redo(name: str) -> ChangeSet:
curr = get_current_operation(name)
redoChild = _query_redo_child(name, curr)
if redoChild == None:
@@ -113,108 +132,127 @@ def execute_redo(name: str) -> None:
return
child = int(redoChild)
redo = _query_redo(name, child)
row = _query_operation(name, child)
redo = row['redo']
_execute(name, redo)
change = _get_change_set(row, False)
write(name, redo)
_update_current_operation(name, curr, child)
# snapshot support to checkout between different version of database
# snapshot is persistent
# since redo always remember the recently undo path
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:
with conn[name].cursor(row_factory=dict_row) as cur:
cur.execute(f"select id from snapshot_operation where tag = '{tag}'")
return cur.rowcount > 0
return _get_operation_by_tag(name, tag) != None
def take_snapshot(name: str, tag: str) -> None:
def take_snapshot(name: str, tag: str) -> int | None:
if tag == None or tag == '':
print('Non empty tag is expected!')
return
return None
curr = get_current_operation(name)
write(name, f"insert into snapshot_operation (id, tag) values ({curr}, '{tag}')")
return curr
with conn[name].cursor() as cur:
cur.execute(f"insert into snapshot_operation (id, tag) values ({curr}, '{tag}')")
def pick_snapshot(name: str, tag: str) -> None:
def pick_snapshot(name: str, tag: str, discard: bool) -> ChangeSet:
if tag == None or tag == '':
print('Non empty tag is expected!')
return
return ChangeSet()
if not have_snapshot(name, tag):
target = _get_operation_by_tag(name, tag)
if target == None:
print('No such snapshot!')
return
return ChangeSet()
curr = get_current_operation(name)
curr_parents = _get_parents(name, curr)
target_parents = _get_parents(name, target)
with conn[name].cursor(row_factory=dict_row) as cur:
cur.execute(f"select id from snapshot_operation where tag = '{tag}'")
target = int(cur.fetchone()['id'])
if target in curr_parents: # target -> curr
for i in range(curr_parents.index(target)):
execute_undo(name)
else:
target_parents = _get_parents(name, target)
if curr in target_parents: # curr -> target
for i in range(target_parents.index(curr)):
execute_redo(name)
else:
ancestor_index = -1
while curr_parents[ancestor_index] == target_parents[ancestor_index]:
ancestor_index -= 1
change = ChangeSet()
# ancestor -> curr
ancestor = curr_parents[ancestor_index + 1] # ancestor_index + 1 is common parent
for i in range(curr_parents.index(ancestor)):
execute_undo(name)
# ancestor -> redo, need assign redo_child
while target_parents[ancestor_index] != target:
cur.execute(f"update operation set redo_child = '{target_parents[ancestor_index]}' where id = '{target_parents[ancestor_index + 1]}'")
execute_redo(name)
ancestor_index -= 1
cur.execute(f"update operation set redo_child = '{target}' where id = '{target_parents[1]}'")
execute_redo(name)
if target in curr_parents:
for _ in range(curr_parents.index(target)):
change.append(execute_undo(name, discard))
# transaction is volatile, commit/abort will destroy transaction.
# can not redo an aborted transaction.
# but can undo a committed transaction... inconsistent...
# it may remove snapshot if it is in aborted transaction
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))
def have_transaction(name: str) -> bool:
with conn[name].cursor(row_factory=dict_row) as cur:
cur.execute(f"select * from transaction_operation")
return cur.rowcount > 0
else:
ancestor_index = -1
while curr_parents[ancestor_index] == target_parents[ancestor_index]:
ancestor_index -= 1
ancestor = curr_parents[ancestor_index + 1]
def start_transaction(name: str, strict: bool = False) -> None:
if have_transaction(name):
print("Only support single transaction now, please commit/abort current transaction!")
return
for _ in range(curr_parents.index(ancestor)):
change.append(execute_undo(name, discard))
curr = get_current_operation(name)
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))
with conn[name].cursor() as cur:
cur.execute(f"insert into transaction_operation (id, strict) values ({curr}, {strict});")
return change.compress()
def commit_transaction(name: str) -> None:
if not have_transaction(name):
print("No active transaction!")
return
_remove_transaction(name)
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 abort_transaction(name: str) -> None:
if not have_transaction(name):
print("No active transaction!")
return
tran = _get_current_transaction_id(name)
def get_current_change_set(name: str) -> ChangeSet:
return get_change_set(name, get_current_operation(name), False)
curr = get_current_operation(name)
curr_parents = _get_parents(name, curr)
for i in range(curr_parents.index(tran)):
execute_undo(name, True)
_remove_transaction(name)
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()