diff --git a/entropylab_qpudb/_qpudatabase.py b/entropylab_qpudb/_qpudatabase.py index 8c3e587..2a30d1d 100644 --- a/entropylab_qpudb/_qpudatabase.py +++ b/entropylab_qpudb/_qpudatabase.py @@ -1,6 +1,7 @@ import dataclasses import json import os +from copy import deepcopy from dataclasses import dataclass from datetime import datetime from enum import Enum, auto @@ -133,35 +134,45 @@ def deserialize_function(snapshot: str, class_object: Type): def __init__(self, dbname, history_index=None, path=None): if path is None: path = os.getcwd() - dbfilename = _db_file_from_path(path, dbname) - histfilename = _hist_file_from_path(path, dbname) + self._path = path + self._dbname = dbname + dbfilename = _db_file_from_path(self._path, self._dbname) if not os.path.exists(dbfilename): - raise FileNotFoundError(f"QPU DB {dbname} does not exist") + raise FileNotFoundError(f"QPU DB {self._dbname} does not exist") + self._db = None super().__init__() - self._dbname = dbname - self._history_index = history_index - db_hist = ZODB.DB(histfilename) - self._con_hist = db_hist.open( - transaction_manager=transaction.TransactionManager() - ) - self._con_hist.transaction_manager.begin() + self._con_hist = self.open_hist_db() + self._readonly, self._con = self.open_data_db(history_index) + + def open_data_db(self, history_index): + dbfilename = _db_file_from_path(self._path, self._dbname) hist_entries = self._con_hist.root()["entries"] - if self._history_index is not None: - self._readonly = True - message_index = self._history_index - at = self._con_hist.root()["entries"][self._history_index]["timestamp"] + if history_index is not None: + readonly = True + message_index = history_index + at = self._con_hist.root()["entries"][history_index]["timestamp"] else: - self._readonly = False + readonly = False message_index = len(hist_entries) - 1 at = None - db = ZODB.DB(dbfilename) - self._con = db.open(transaction_manager=transaction.TransactionManager(), at=at) - assert self._con.isReadOnly() == self._readonly, "internal error: Inconsistent readonly state" - self._con.transaction_manager.begin() + self._db = ZODB.DB(dbfilename) if self._db is None else self._db + con = self._db.open(transaction_manager=transaction.TransactionManager(), at=at) + assert con.isReadOnly() == readonly, "internal error: Inconsistent readonly state" + con.transaction_manager.begin() print( - f"opening qpu database {dbname} from " + f"opening qpu database {self._dbname} from " f"commit {self._str_hist_entry(hist_entries[message_index])} at index {message_index}" ) + return readonly, con + + def open_hist_db(self): + histfilename = _hist_file_from_path(self._path, self._dbname) + db_hist = ZODB.DB(histfilename) + con_hist = db_hist.open( + transaction_manager=transaction.TransactionManager() + ) + con_hist.transaction_manager.begin() + return con_hist def __enter__(self): return self @@ -248,16 +259,21 @@ def commit(self, message: Optional[str] = None) -> None: """ if self._readonly: raise ReadOnlyError("Attempting to commit to a DB in a readonly state") + lt_before = self._con._db.lastTransaction() self._con.transaction_manager.commit() - hist_root = self._con_hist.root() - hist_entries = hist_root["entries"] - now = datetime.utcnow() - hist_entries.append({"timestamp": now, "message": message}) - self._con_hist.transaction_manager.commit() - print( - f"commiting qpu database {self._dbname} " - f"with commit {self._str_hist_entry(hist_entries[-1])} at index {len(hist_entries) - 1}" - ) + lt_after = self._con._db.lastTransaction() + if lt_before != lt_after: # this means a commit actually took place + hist_root = self._con_hist.root() + hist_entries = hist_root["entries"] + now = datetime.utcnow() + hist_entries.append({"timestamp": now, "message": message}) + self._con_hist.transaction_manager.commit() + print( + f"commiting qpu database {self._dbname} " + f"with commit {self._str_hist_entry(hist_entries[-1])} at index {len(hist_entries) - 1}" + ) + else: + print('did not commit') def abort(self): self._con.transaction_manager.abort() @@ -283,6 +299,18 @@ def get_history(self) -> pd.DataFrame: def _str_hist_entry(hist_entry): return f"" + def restore_from_history(self, history_index: int) -> None: + """ + restore the current unmodified and open DB data to be the same as the one from `history_index`. + Will not commit the restored data. + + :param history_index: History index from which to restore + """ + readonly, con = self.open_data_db(history_index) + self._con.root()["elements"] = deepcopy(con.root()["elements"]) + con.close() + print(con) + class QpuDatabaseConnection(QpuDatabaseConnectionBase): def __init__(self, dbname, resolver, **kwargs): diff --git a/entropylab_qpudb/tests/test_qpudatabase.py b/entropylab_qpudb/tests/test_qpudatabase.py index a0f71b7..c36a3c3 100644 --- a/entropylab_qpudb/tests/test_qpudatabase.py +++ b/entropylab_qpudb/tests/test_qpudatabase.py @@ -99,9 +99,11 @@ def test_simple_set_with_commit(testdb): db.set("q2", "p1", 11.0) print(db.get("q2", "p1")) db.commit() + assert len(db.get_history()) == 2 with QpuDatabaseConnectionBase(testdb) as db: print() + assert len(db.get_history()) == 2 print(db.get("q2", "p1")) assert db.get("q2", "p1").value == 11.0 @@ -118,11 +120,14 @@ def test_set_with_commit_multiple(testdb): print() db.set("q2", "p1", 11.0) print(db.get("q2", "p1")) + assert len(db.get_history()) == 1 db.commit() + assert len(db.get_history()) == 2 with QpuDatabaseConnectionBase(testdb) as db: db.set("q1", "p1", [1, 2]) db.commit() + assert len(db.get_history()) == 3 with QpuDatabaseConnectionBase(testdb) as db: print() @@ -188,6 +193,36 @@ def test_fail_on_commit_to_readonly(testdb): db.commit('trying') +def test_commit_unmodified(testdb): + with QpuDatabaseConnection(testdb, simp_resolver) as db: + print() + print(_parseRaw(db._con._db.lastTransaction())) + assert len(db.get_history()) == 1 + db.commit("my first commit - unmodified") + print(_parseRaw(db._con._db.lastTransaction())) + assert len(db.get_history()) == 1 + + +def test_restore_from_history(testdb): + with QpuDatabaseConnection(testdb, simp_resolver) as db: + print() + db.set("q2", "p1", 11.0) + print(db.get("q2", "p1")) + db.commit("my first commit") + + with QpuDatabaseConnection(testdb, simp_resolver) as db: + print() + assert db.get("q2", "p1").value == 11.0 + db.restore_from_history(0) + print(db.get("q2", "p1")) + assert db.get("q2", "p1").value == 3.4 + + # make sure wasn't committed + with QpuDatabaseConnection(testdb, simp_resolver) as db: + print() + assert db.get("q2", "p1").value == 11.0 + + def test_print(testdb): with QpuDatabaseConnectionBase(testdb) as db: print() diff --git a/pyproject.toml b/pyproject.toml index 54125d9..5bcf20f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "entropylab-qpudb" -version = "0.0.1-alpha0" +version = "0.0.3" description = "A extension of entropy lab for persistent storage of calibration parameters of a quantum processing unit (QPU)" authors = ["Lior Ella "]