def test_atomic_on_connection_plus_that_is_in_progress(in_transaction): sqlite_conn = sqlite3.connect(':memory:') conn_plus = ConnectionPlus(sqlite_conn) # explicitly set to True for testing purposes conn_plus.atomic_in_progress = True # implement parametrizing over connection's `in_transaction` attribute if in_transaction: conn_plus.cursor().execute('BEGIN') assert in_transaction is conn_plus.in_transaction isolation_level = conn_plus.isolation_level in_transaction = conn_plus.in_transaction with atomic(conn_plus) as atomic_conn: assert True is conn_plus.atomic_in_progress assert isolation_level == conn_plus.isolation_level assert in_transaction is conn_plus.in_transaction assert True is atomic_conn.atomic_in_progress assert isolation_level == atomic_conn.isolation_level assert in_transaction is atomic_conn.in_transaction assert True is conn_plus.atomic_in_progress assert isolation_level == conn_plus.isolation_level assert in_transaction is conn_plus.in_transaction assert True is atomic_conn.atomic_in_progress assert isolation_level == atomic_conn.isolation_level assert in_transaction is atomic_conn.in_transaction
def _populate_results_table(source_conn: ConnectionPlus, target_conn: ConnectionPlus, source_table_name: str, target_table_name: str) -> None: """ Copy over all the entries of the results table """ get_data_query = f""" SELECT * FROM "{source_table_name}" """ source_cursor = source_conn.cursor() target_cursor = target_conn.cursor() for row in source_cursor.execute(get_data_query): column_names = ','.join(row.keys()[1:]) # the first key is "id" values = tuple(val for val in row[1:]) value_placeholders = sql_placeholder_string(len(values)) insert_data_query = f""" INSERT INTO "{target_table_name}" ({column_names}) values {value_placeholders} """ target_cursor.execute(insert_data_query, values)
def connect(name: Union[str, Path], debug: bool = False, version: int = -1) -> ConnectionPlus: """ Connect or create database. If debug the queries will be echoed back. This function takes care of registering the numpy/sqlite type converters that we need. Args: name: name or path to the sqlite file debug: whether or not to turn on tracing version: which version to create. We count from 0. -1 means 'latest'. Should always be left at -1 except when testing. Returns: conn: connection object to the database (note, it is `ConnectionPlus`, not `sqlite3.Connection` """ # register numpy->binary(TEXT) adapter sqlite3.register_adapter(np.ndarray, _adapt_array) # register binary(TEXT) -> numpy converter sqlite3.register_converter("array", _convert_array) sqlite3_conn = sqlite3.connect(name, detect_types=sqlite3.PARSE_DECLTYPES, check_same_thread=True) conn = ConnectionPlus(sqlite3_conn) latest_supported_version = _latest_available_version() db_version = get_user_version(conn) if db_version > latest_supported_version: raise RuntimeError(f"Database {name} is version {db_version} but this " f"version of QCoDeS supports up to " f"version {latest_supported_version}") # sqlite3 options conn.row_factory = sqlite3.Row # Make sure numpy ints and floats types are inserted properly for numpy_int in numpy_ints: sqlite3.register_adapter(numpy_int, int) sqlite3.register_converter("numeric", _convert_numeric) for numpy_float in (float,) + numpy_floats: sqlite3.register_adapter(numpy_float, _adapt_float) for complex_type in complex_types: sqlite3.register_adapter(complex_type, _adapt_complex) sqlite3.register_converter("complex", _convert_complex) if debug: conn.set_trace_callback(print) init_db(conn) perform_db_upgrade(conn, version=version) return conn
def test_atomic_on_outmost_connection_that_is_in_transaction(): conn = ConnectionPlus(sqlite3.connect(':memory:')) conn.execute('BEGIN') assert True is conn.in_transaction match_str = re.escape('SQLite connection has uncommitted transactions. ' 'Please commit those before starting an atomic ' 'transaction.') with pytest.raises(RuntimeError, match=match_str): with atomic(conn): pass
def test_connection_plus(): sqlite_conn = sqlite3.connect(':memory:') conn_plus = ConnectionPlus(sqlite_conn) assert isinstance(conn_plus, ConnectionPlus) assert isinstance(conn_plus, sqlite3.Connection) assert False is conn_plus.atomic_in_progress match_str = re.escape('Attempted to create `ConnectionPlus` from a ' '`ConnectionPlus` object which is not allowed.') with pytest.raises(ValueError, match=match_str): ConnectionPlus(conn_plus)
def _update_run_description(conn: ConnectionPlus, run_id: int, description: str) -> None: """ Update the run_description field for the given run_id. The description string is NOT validated. """ sql = """ UPDATE runs SET run_description = ? WHERE run_id = ? """ with atomic(conn) as conn: conn.cursor().execute(sql, (description, run_id))
def set_run_timestamp(conn: ConnectionPlus, run_id: int) -> None: """ Set the run_timestamp for the run with the given run_id. If the run_timestamp has already been set, a RuntimeError is raised. """ query = """ SELECT run_timestamp FROM runs WHERE run_id = ? """ cmd = """ UPDATE runs SET run_timestamp = ? WHERE run_id = ? """ with atomic(conn) as conn: c = conn.cursor() timestamp = one(c.execute(query, (run_id, )), 'run_timestamp') if timestamp is not None: raise RuntimeError('Can not set run_timestamp; it has already ' f'been set to: {timestamp}') else: current_time = time.time() c.execute(cmd, (current_time, run_id)) log.info(f"Set the run_timestamp of run_id {run_id} to " f"{current_time}")
def get_runid_from_guid(conn: ConnectionPlus, guid: str) -> Union[int, None]: """ Get the run_id of a run based on the guid Args: conn: connection to the database guid: the guid to look up Returns: The run_id if found, else -1. Raises: RuntimeError if more than one run with the given GUID exists """ query = """ SELECT run_id FROM runs WHERE guid = ? """ cursor = conn.cursor() cursor.execute(query, (guid, )) rows = cursor.fetchall() if len(rows) == 0: run_id = -1 elif len(rows) > 1: errormssg = ('Critical consistency error: multiple runs with' f' the same GUID found! {len(rows)} runs have GUID ' f'{guid}') log.critical(errormssg) raise RuntimeError(errormssg) else: run_id = int(rows[0]['run_id']) return run_id
def test_make_connection_plus_from_connecton_plus(): conn = ConnectionPlus(sqlite3.connect(':memory:')) conn_plus = make_connection_plus_from(conn) assert isinstance(conn_plus, ConnectionPlus) assert conn.atomic_in_progress is conn_plus.atomic_in_progress assert conn_plus is conn
def test_two_nested_atomics(): sqlite_conn = sqlite3.connect(':memory:') conn_plus = ConnectionPlus(sqlite_conn) atomic_in_progress = conn_plus.atomic_in_progress isolation_level = conn_plus.isolation_level assert False is conn_plus.in_transaction with atomic(conn_plus) as atomic_conn_1: assert conn_plus_in_transaction(conn_plus) assert conn_plus_in_transaction(atomic_conn_1) with atomic(atomic_conn_1) as atomic_conn_2: assert conn_plus_in_transaction(conn_plus) assert conn_plus_in_transaction(atomic_conn_1) assert conn_plus_in_transaction(atomic_conn_2) assert conn_plus_in_transaction(conn_plus) assert conn_plus_in_transaction(atomic_conn_1) assert conn_plus_in_transaction(atomic_conn_2) assert conn_plus_is_idle(conn_plus, isolation_level) assert conn_plus_is_idle(atomic_conn_1, isolation_level) assert conn_plus_is_idle(atomic_conn_2, isolation_level) assert atomic_in_progress == conn_plus.atomic_in_progress assert atomic_in_progress == atomic_conn_1.atomic_in_progress assert atomic_in_progress == atomic_conn_2.atomic_in_progress
def get_metadata_from_run_id(conn: ConnectionPlus, run_id: int) -> Dict: """ Get all metadata associated with the specified run """ # TODO: promote snapshot to be present at creation time non_metadata = RUNS_TABLE_COLUMNS + ['snapshot'] metadata = {} possible_tags = [] # first fetch all columns of the runs table query = "PRAGMA table_info(runs)" cursor = conn.cursor() for row in cursor.execute(query): if row['name'] not in non_metadata: possible_tags.append(row['name']) # and then fetch whatever metadata the run might have for tag in possible_tags: query = f""" SELECT "{tag}" FROM runs WHERE run_id = ? AND "{tag}" IS NOT NULL """ cursor.execute(query, (run_id, )) row = cursor.fetchall() if row != []: metadata[tag] = row[0][tag] return metadata
def is_run_id_in_database(conn: ConnectionPlus, *run_ids) -> Dict[int, bool]: """ Look up run_ids and return a dictionary with the answers to the question "is this run_id in the database?" Args: conn: the connection to the database run_ids: the run_ids to look up Returns: a dict with the run_ids as keys and bools as values. True means that the run_id DOES exist in the database """ run_ids = np.unique(run_ids) placeholders = sql_placeholder_string(len(run_ids)) query = f""" SELECT run_id FROM runs WHERE run_id in {placeholders} """ cursor = conn.cursor() cursor.execute(query, run_ids) rows = cursor.fetchall() existing_ids = [row[0] for row in rows] return {run_id: (run_id in existing_ids) for run_id in run_ids}
def test_atomic(): sqlite_conn = sqlite3.connect(':memory:') match_str = re.escape('atomic context manager only accepts ConnectionPlus ' 'database connection objects.') with pytest.raises(ValueError, match=match_str): with atomic(sqlite_conn): pass conn_plus = ConnectionPlus(sqlite_conn) assert False is conn_plus.atomic_in_progress atomic_in_progress = conn_plus.atomic_in_progress isolation_level = conn_plus.isolation_level assert False is conn_plus.in_transaction with atomic(conn_plus) as atomic_conn: assert conn_plus_in_transaction(atomic_conn) assert conn_plus_in_transaction(conn_plus) assert isolation_level == conn_plus.isolation_level assert False is conn_plus.in_transaction assert atomic_in_progress is conn_plus.atomic_in_progress assert isolation_level == conn_plus.isolation_level assert False is atomic_conn.in_transaction assert atomic_in_progress is atomic_conn.atomic_in_progress
def get_parameters(conn: ConnectionPlus, run_id: int) -> List[ParamSpec]: """ Get the list of param specs for run Args: conn: the connection to the sqlite database run_id: The id of the run Returns: A list of param specs for this run """ sql = f""" SELECT parameter FROM layouts WHERE run_id={run_id} """ c = conn.execute(sql) param_names_temp = many_many(c, 'parameter') param_names = [p[0] for p in param_names_temp] param_names = cast(List[str], param_names) parspecs = [] for param_name in param_names: parspecs.append(get_paramspec(conn, run_id, param_name)) return parspecs
def path_to_dbfile(conn: ConnectionPlus) -> str: """ Return the path of the database file that the conn object is connected to """ cursor = conn.cursor() cursor.execute("PRAGMA database_list") row = cursor.fetchall()[0] return row[2]
def update_run_description(conn: ConnectionPlus, run_id: int, description: str) -> None: """ Update the run_description field for the given run_id. The description string must be a valid JSON string representation of a RunDescriber object """ try: RunDescriber.from_json(description) except Exception as e: raise ValueError("Invalid description string. Must be a JSON string " "representaion of a RunDescriber object.") from e sql = """ UPDATE runs SET run_description = ? WHERE run_id = ? """ with atomic(conn) as conn: conn.cursor().execute(sql, (description, run_id))
def _2to3_get_result_tables(conn: ConnectionPlus) -> Dict[int, str]: rst_query = "SELECT run_id, result_table_name FROM runs" cur = conn.cursor() cur.execute(rst_query) data = cur.fetchall() cur.close() results = {} for row in data: results[row['run_id']] = row['result_table_name'] return results
def _rewrite_timestamps(target_conn: ConnectionPlus, target_run_id: int, correct_run_timestamp: Optional[float], correct_completed_timestamp: Optional[float]) -> None: """ Update the timestamp to match the original one """ query = """ UPDATE runs SET run_timestamp = ? WHERE run_id = ? """ cursor = target_conn.cursor() cursor.execute(query, (correct_run_timestamp, target_run_id)) query = """ UPDATE runs SET completed_timestamp = ? WHERE run_id = ? """ cursor = target_conn.cursor() cursor.execute(query, (correct_completed_timestamp, target_run_id))
def _2to3_get_layouts( conn: ConnectionPlus) -> Dict[int, Tuple[str, str, str, str]]: query = """ SELECT layout_id, parameter, label, unit, inferred_from FROM layouts """ cur = conn.cursor() cur.execute(query) results: Dict[int, Tuple[str, str, str, str]] = {} for row in cur.fetchall(): results[row['layout_id']] = (row['parameter'], row['label'], row['unit'], row['inferred_from']) return results
def test_atomic_transaction(tmp_path): """Test that atomic_transaction works for ConnectionPlus""" dbfile = str(tmp_path / 'temp.db') conn = ConnectionPlus(sqlite3.connect(dbfile)) ctrl_conn = sqlite3.connect(dbfile) sql_create_table = 'CREATE TABLE smth (name TEXT)' sql_table_exists = 'SELECT sql FROM sqlite_master WHERE TYPE = "table"' atomic_transaction(conn, sql_create_table) assert sql_create_table in ctrl_conn.execute( sql_table_exists).fetchall()[0]
def test_atomic_with_exception(): sqlite_conn = sqlite3.connect(':memory:') conn_plus = ConnectionPlus(sqlite_conn) sqlite_conn.execute('PRAGMA user_version(25)') sqlite_conn.commit() assert 25 == sqlite_conn.execute('PRAGMA user_version').fetchall()[0][0] with pytest.raises(RuntimeError, match="Rolling back due to unhandled exception") as e: with atomic(conn_plus) as atomic_conn: atomic_conn.execute('PRAGMA user_version(42)') raise Exception('intended exception') assert error_caused_by(e, 'intended exception') assert 25 == sqlite_conn.execute('PRAGMA user_version').fetchall()[0][0]
def set_journal_mode(conn: ConnectionPlus, journal_mode: JournalMode) -> None: """ Set the ``atomic commit and rollback mode`` of the sqlite database. See https://www.sqlite.org/pragma.html#pragma_journal_mode for details. Args: conn: Connection to the database. journal_mode: Which `journal_mode` should be used for atomic commit and rollback. Options are DELETE, TRUNCATE, PERSIST, MEMORY, WAL and OFF. """ valid_journal_modes = ["DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"] if journal_mode not in valid_journal_modes: raise RuntimeError(f"Invalid journal_mode {journal_mode} " f"Valid modes are {valid_journal_modes}") query = f"PRAGMA journal_mode={journal_mode};" cursor = conn.cursor() cursor.execute(query)
def get_matching_exp_ids(conn: ConnectionPlus, **match_conditions) -> List: """ Get exp_ids for experiments matching the match_conditions Raises: ValueError if a match_condition that is not "name", "sample_name", "format_string", "run_counter", "start_time", or "end_time" """ valid_conditions = [ "name", "sample_name", "start_time", "end_time", "run_counter", "format_string" ] for mcond in match_conditions: if mcond not in valid_conditions: raise ValueError(f"{mcond} is not a valid match condition.") end_time = match_conditions.get('end_time', None) time_eq = "=" if end_time is not None else "IS" sample_name = match_conditions.get('sample_name', None) sample_name_eq = "=" if sample_name is not None else "IS" query = "SELECT exp_id FROM experiments " for n, mcond in enumerate(match_conditions): if n == 0: query += f"WHERE {mcond} = ? " else: query += f"AND {mcond} = ? " # now some syntax clean-up if "format_string" in match_conditions: format_string = match_conditions["format_string"] query = query.replace("format_string = ?", f'format_string = "{format_string}"') match_conditions.pop("format_string") query = query.replace("end_time = ?", f"end_time {time_eq} ?") query = query.replace("sample_name = ?", f"sample_name {sample_name_eq} ?") cursor = conn.cursor() cursor.execute(query, tuple(match_conditions.values())) rows = cursor.fetchall() return [row[0] for row in rows]
def _2to3_get_deps(conn: ConnectionPlus) -> DefaultDict[int, List[int]]: query = """ SELECT layouts.run_id, layouts.layout_id FROM layouts INNER JOIN dependencies ON layouts.layout_id==dependencies.dependent """ cur = conn.cursor() cur.execute(query) data = cur.fetchall() cur.close() results: DefaultDict[int, List[int]] = defaultdict(list) for row in data: run_id = row['run_id'] layout_id = row['layout_id'] results[run_id].append(layout_id) return results
def test_dataset_in_memory_does_not_create_runs_table( meas_with_registered_param, DMM, DAC, tmp_path): with meas_with_registered_param.run( dataset_class=DataSetType.DataSetInMem) as datasaver: for set_v in np.linspace(0, 25, 10): DAC.ch1.set(set_v) get_v = DMM.v1() datasaver.add_result((DAC.ch1, set_v), (DMM.v1, get_v)) ds = datasaver.dataset dbfile = datasaver.dataset._path_to_db conn = ConnectionPlus(sqlite3.connect(dbfile)) tables_query = 'SELECT * FROM sqlite_master WHERE TYPE = "table"' tables = list(atomic_transaction(conn, tables_query).fetchall()) assert len(tables) == 4 tablenames = tuple(table[1] for table in tables) assert all(ds.name not in table_name for table_name in tablenames)
def _2to3_get_dependencies( conn: ConnectionPlus) -> DefaultDict[int, List[int]]: query = """ SELECT dependent, independent FROM dependencies ORDER BY dependent, axis_num ASC """ cur = conn.cursor() cur.execute(query) data = cur.fetchall() cur.close() results: DefaultDict[int, List[int]] = defaultdict(list) if len(data) == 0: return results for row in data: dep = row['dependent'] indep = row['independent'] results[dep].append(indep) return results
def get_exp_ids_from_run_ids(conn: ConnectionPlus, run_ids: Sequence[int]) -> List[int]: """ Get the corresponding exp_id for a sequence of run_ids Args: conn: connection to the database run_ids: a sequence of the run_ids to get the exp_id of Returns: A list of exp_ids matching the run_ids """ sql_placeholders = sql_placeholder_string(len(run_ids)) exp_id_query = f""" SELECT exp_id FROM runs WHERE run_id IN {sql_placeholders} """ cursor = conn.cursor() cursor.execute(exp_id_query, run_ids) rows = cursor.fetchall() return [exp_id for row in rows for exp_id in row]
def upgrade_2_to_3(conn: ConnectionPlus) -> None: """ Perform the upgrade from version 2 to version 3 Insert a new column, run_description, to the runs table and fill it out for exisitng runs with information retrieved from the layouts and dependencies tables represented as the json output of a RunDescriber object """ no_of_runs_query = "SELECT max(run_id) FROM runs" no_of_runs = one(atomic_transaction(conn, no_of_runs_query), 'max(run_id)') no_of_runs = no_of_runs or 0 # If one run fails, we want the whole upgrade to roll back, hence the # entire upgrade is one atomic transaction with atomic(conn) as conn: sql = "ALTER TABLE runs ADD COLUMN run_description TEXT" transaction(conn, sql) result_tables = _2to3_get_result_tables(conn) layout_ids_all = _2to3_get_layout_ids(conn) indeps_all = _2to3_get_indeps(conn) deps_all = _2to3_get_deps(conn) layouts = _2to3_get_layouts(conn) dependencies = _2to3_get_dependencies(conn) pbar = tqdm(range(1, no_of_runs + 1)) pbar.set_description("Upgrading database") for run_id in pbar: if run_id in layout_ids_all: result_table_name = result_tables[run_id] layout_ids = list(layout_ids_all[run_id]) if run_id in indeps_all: independents = tuple(indeps_all[run_id]) else: independents = () if run_id in deps_all: dependents = tuple(deps_all[run_id]) else: dependents = () paramspecs = _2to3_get_paramspecs(conn, layout_ids, layouts, dependencies, dependents, independents, result_table_name) interdeps = InterDependencies(*paramspecs.values()) desc_dict = {'interdependencies': interdeps._to_dict()} json_str = json.dumps(desc_dict) else: desc_dict = { 'interdependencies': InterDependencies()._to_dict() } json_str = json.dumps(desc_dict) sql = f""" UPDATE runs SET run_description = ? WHERE run_id == ? """ cur = conn.cursor() cur.execute(sql, (json_str, run_id)) log.debug(f"Upgrade in transition, run number {run_id}: OK")
def upgrade_3_to_4(conn: ConnectionPlus) -> None: """ Perform the upgrade from version 3 to version 4. This really repeats the version 3 upgrade as it originally had two bugs in the inferred annotation. inferred_from was passed incorrectly resulting in the parameter being marked inferred_from for each char in the inferred_from variable and inferred_from was not handled correctly for parameters that were neither dependencies nor dependent on other parameters. Both have since been fixed so rerun the upgrade. """ no_of_runs_query = "SELECT max(run_id) FROM runs" no_of_runs = one(atomic_transaction(conn, no_of_runs_query), 'max(run_id)') no_of_runs = no_of_runs or 0 # If one run fails, we want the whole upgrade to roll back, hence the # entire upgrade is one atomic transaction with atomic(conn) as conn: result_tables = _2to3_get_result_tables(conn) layout_ids_all = _2to3_get_layout_ids(conn) indeps_all = _2to3_get_indeps(conn) deps_all = _2to3_get_deps(conn) layouts = _2to3_get_layouts(conn) dependencies = _2to3_get_dependencies(conn) pbar = tqdm(range(1, no_of_runs + 1), file=sys.stdout) pbar.set_description("Upgrading database; v3 -> v4") for run_id in pbar: if run_id in layout_ids_all: result_table_name = result_tables[run_id] layout_ids = list(layout_ids_all[run_id]) if run_id in indeps_all: independents = tuple(indeps_all[run_id]) else: independents = () if run_id in deps_all: dependents = tuple(deps_all[run_id]) else: dependents = () paramspecs = _2to3_get_paramspecs(conn, layout_ids, layouts, dependencies, dependents, independents, result_table_name) interdeps = InterDependencies(*paramspecs.values()) desc_dict = {'interdependencies': interdeps._to_dict()} json_str = json.dumps(desc_dict) else: desc_dict = { 'interdependencies': InterDependencies()._to_dict() } json_str = json.dumps(desc_dict) sql = f""" UPDATE runs SET run_description = ? WHERE run_id == ? """ cur = conn.cursor() cur.execute(sql, (json_str, run_id)) log.debug(f"Upgrade in transition, run number {run_id}: OK")
def connect(name: str, debug: bool = False, version: int = -1) -> ConnectionPlus: """ Connect or create database. If debug the queries will be echoed back. This function takes care of registering the numpy/sqlite type converters that we need. Args: name: name or path to the sqlite file debug: whether or not to turn on tracing version: which version to create. We count from 0. -1 means 'latest'. Should always be left at -1 except when testing. Returns: conn: connection object to the database (note, it is `ConnectionPlus`, not `sqlite3.Connection` """ # register numpy->binary(TEXT) adapter # the typing here is ignored due to what we think is a flaw in typeshed # see https://github.com/python/typeshed/issues/2429 sqlite3.register_adapter(np.ndarray, _adapt_array) # type: ignore # register binary(TEXT) -> numpy converter # for some reasons mypy complains about this sqlite3.register_converter("array", _convert_array) sqlite3_conn = sqlite3.connect(name, detect_types=sqlite3.PARSE_DECLTYPES) conn = ConnectionPlus(sqlite3_conn) latest_supported_version = _latest_available_version() db_version = get_user_version(conn) if db_version > latest_supported_version: raise RuntimeError(f"Database {name} is version {db_version} but this " f"version of QCoDeS supports up to " f"version {latest_supported_version}") # sqlite3 options conn.row_factory = sqlite3.Row # Make sure numpy ints and floats types are inserted properly for numpy_int in [ np.int, np.int8, np.int16, np.int32, np.int64, np.uint, np.uint8, np.uint16, np.uint32, np.uint64 ]: sqlite3.register_adapter(numpy_int, int) sqlite3.register_converter("numeric", _convert_numeric) for numpy_float in [np.float, np.float16, np.float32, np.float64]: sqlite3.register_adapter(numpy_float, _adapt_float) for complex_type in complex_types: sqlite3.register_adapter(complex_type, _adapt_complex) # type: ignore sqlite3.register_converter("complex", _convert_complex) if debug: conn.set_trace_callback(print) init_db(conn) perform_db_upgrade(conn, version=version) return conn