Skip to content

Commit a4b7885

Browse files
Mikhail AstafevCopilot
andcommitted
Address review: document snapshot/count caveats, isolate tests from config
- Document in the docstring and newsfragment that snapshots are not read (they can be large and slow) and that the record count may be less precise than DataSet.number_of_results. - Rewrite the tests to build the database via an explicit connection object (load_or_create_experiment(conn=...)/new_data_set(conn=...)) so the global QCoDeS config is not mutated, and close the connection via request.addfinalizer so it is cleaned up even if a test fails. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 151e4eb commit a4b7885

3 files changed

Lines changed: 91 additions & 97 deletions

File tree

docs/changes/newsfragments/8266.new

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@ record counts and guids) via a single ``JOIN`` query on the ``runs`` and
44
``experiments`` tables, without instantiating a ``DataSet`` object per run,
55
making it possible to list databases with many thousands of runs almost
66
instantly. The returned :class:`qcodes.dataset.RunOverviewDict` is also exported
7-
on the public ``qcodes.dataset`` namespace.
7+
on the public ``qcodes.dataset`` namespace. The function does not return
8+
snapshots as they can be large and slow down building the database overview.
9+
The number of results in each dataset is taken from shape information when
10+
available, and otherwise falls back to a best-effort count of the rows in the
11+
so-called results table that exists for every dataset, which may NOT be as
12+
precise as ``DataSet.number_of_results``.

src/qcodes/dataset/sqlite/db_overview.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,13 @@ def get_db_overview(
128128
tables to fetch run metadata, avoiding the much more expensive
129129
``experiments()`` + ``data_sets()`` enumeration that instantiates a
130130
``DataSet`` object per run. It is therefore well suited for listing the
131-
contents of databases with many runs.
131+
contents of databases with many runs. The (potentially large) snapshot of
132+
each run is deliberately not read, as it would slow down building the
133+
overview significantly.
132134
133135
The reported number of ``records`` is a best-effort estimate of the number
134-
of data points in a run:
136+
of data points in a run and may be less precise than
137+
``DataSet.number_of_results``:
135138
136139
* For completed runs the shape information stored in the run description is
137140
preferred (it is the authoritative final count), falling back to the

tests/dataset/test_db_overview.py

Lines changed: 80 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@
33
from __future__ import annotations
44

55
import json
6-
import sqlite3
76
from typing import TYPE_CHECKING
87

98
import pytest
109

1110
from qcodes.dataset import (
1211
get_db_overview,
13-
initialise_or_create_database_at,
1412
load_or_create_experiment,
1513
new_data_set,
1614
)
1715
from qcodes.dataset.descriptions.dependencies import InterDependencies_
16+
from qcodes.dataset.sqlite.connection import path_to_dbfile
1817
from qcodes.dataset.sqlite.database import connect
1918
from qcodes.dataset.sqlite.db_overview import (
2019
_format_timestamp,
@@ -25,29 +24,44 @@
2524
if TYPE_CHECKING:
2625
from pathlib import Path
2726

27+
from qcodes.dataset.sqlite.connection import AtomicConnection
2828

29-
def _make_db_with_runs(
30-
db_path: str,
29+
30+
@pytest.fixture(name="db_conn")
31+
def _db_conn(tmp_path: Path, request: pytest.FixtureRequest) -> AtomicConnection:
32+
"""Connection to a fresh temporary database.
33+
34+
An explicit connection object is used rather than
35+
``initialise_or_create_database_at`` so that the global QCoDeS config is
36+
left untouched. The connection is closed through a finalizer so that it is
37+
cleaned up even if the test fails.
38+
"""
39+
conn = connect(str(tmp_path / "overview.db"))
40+
request.addfinalizer(conn.close)
41+
return conn
42+
43+
44+
def _create_runs(
45+
conn: AtomicConnection,
46+
*,
3147
n_runs: int = 1,
3248
n_points: int = 10,
3349
experiment_name: str = "test_exp",
3450
sample_name: str = "test_sample",
3551
) -> None:
36-
"""Create a database at ``db_path`` with ``n_runs`` simple numeric runs."""
37-
initialise_or_create_database_at(db_path)
38-
load_or_create_experiment(experiment_name, sample_name=sample_name)
52+
"""Create ``n_runs`` simple numeric runs on ``conn``."""
53+
exp = load_or_create_experiment(experiment_name, sample_name=sample_name, conn=conn)
3954
p_x = ParamSpecBase("x", "numeric")
4055
p_y = ParamSpecBase("y", "numeric")
4156
interdeps = InterDependencies_(dependencies={p_y: (p_x,)})
4257

4358
for r in range(n_runs):
44-
ds = new_data_set(f"run_{r + 1}")
59+
ds = new_data_set(f"run_{r + 1}", exp_id=exp.exp_id, conn=conn)
4560
ds.set_interdependencies(interdeps)
4661
ds.mark_started()
4762
for i in range(n_points):
4863
ds.add_results([{p_x.name: float(i), p_y.name: float(i**2)}])
4964
ds.mark_completed()
50-
ds.conn.close()
5165

5266

5367
def test_records_from_run_description() -> None:
@@ -77,11 +91,10 @@ def test_format_timestamp() -> None:
7791
assert time.count(":") == 2
7892

7993

80-
def test_get_db_overview_basic_fields(tmp_path: Path) -> None:
81-
db_path = str(tmp_path / "basic.db")
82-
_make_db_with_runs(db_path, n_runs=2)
94+
def test_get_db_overview_basic_fields(db_conn: AtomicConnection) -> None:
95+
_create_runs(db_conn, n_runs=2)
8396

84-
overview = get_db_overview(db_path)
97+
overview = get_db_overview(path_to_dbfile(db_conn))
8598

8699
assert set(overview.keys()) == {1, 2}
87100
for run_id, info in overview.items():
@@ -94,109 +107,90 @@ def test_get_db_overview_basic_fields(tmp_path: Path) -> None:
94107
assert info["completed_date"] and info["completed_time"]
95108

96109

97-
def test_get_db_overview_counts_result_rows(tmp_path: Path) -> None:
98-
db_path = str(tmp_path / "counts.db")
99-
_make_db_with_runs(db_path, n_runs=3, n_points=10)
110+
def test_get_db_overview_counts_result_rows(db_conn: AtomicConnection) -> None:
111+
_create_runs(db_conn, n_runs=3, n_points=10)
100112

101-
overview = get_db_overview(db_path)
113+
overview = get_db_overview(conn=db_conn)
102114

103-
conn = sqlite3.connect(db_path)
104-
try:
105-
for run_id, info in overview.items():
106-
(table_name,) = conn.execute(
107-
"SELECT result_table_name FROM runs WHERE run_id=?", (run_id,)
108-
).fetchone()
109-
(actual,) = conn.execute(f'SELECT COUNT(*) FROM "{table_name}"').fetchone()
110-
assert info["records"] == actual == 10
111-
finally:
112-
conn.close()
115+
for run_id, info in overview.items():
116+
(table_name,) = db_conn.execute(
117+
"SELECT result_table_name FROM runs WHERE run_id=?", (run_id,)
118+
).fetchone()
119+
(actual,) = db_conn.execute(f'SELECT COUNT(*) FROM "{table_name}"').fetchone()
120+
assert info["records"] == actual == 10
113121

114122

115-
def test_get_db_overview_incremental(tmp_path: Path) -> None:
116-
db_path = str(tmp_path / "incremental.db")
117-
_make_db_with_runs(db_path, n_runs=2)
123+
def test_get_db_overview_incremental(db_conn: AtomicConnection) -> None:
124+
_create_runs(db_conn, n_runs=2)
125+
path = path_to_dbfile(db_conn)
118126

119-
assert set(get_db_overview(db_path).keys()) == {1, 2}
120-
assert get_db_overview(db_path, start_run_id=2) == {}
127+
assert set(get_db_overview(path).keys()) == {1, 2}
128+
assert get_db_overview(path, start_run_id=2) == {}
121129

122-
_make_db_with_runs(db_path, n_runs=1, experiment_name="test_exp2", sample_name="s2")
130+
_create_runs(db_conn, n_runs=1, experiment_name="test_exp2", sample_name="s2")
123131

124-
incremental = get_db_overview(db_path, start_run_id=2)
132+
incremental = get_db_overview(path, start_run_id=2)
125133
assert set(incremental.keys()) == {3}
126134
assert incremental[3]["experiment"] == "test_exp2"
127135

128136

129-
def test_get_db_overview_extra_columns(tmp_path: Path) -> None:
130-
db_path = str(tmp_path / "extra.db")
131-
initialise_or_create_database_at(db_path)
132-
load_or_create_experiment("exp", sample_name="sample")
137+
def test_get_db_overview_extra_columns(db_conn: AtomicConnection) -> None:
138+
exp = load_or_create_experiment("exp", sample_name="sample", conn=db_conn)
133139
p_x = ParamSpecBase("x", "numeric")
134140
p_y = ParamSpecBase("y", "numeric")
135141
interdeps = InterDependencies_(dependencies={p_y: (p_x,)})
136-
ds = new_data_set("tagged_run")
142+
ds = new_data_set("tagged_run", exp_id=exp.exp_id, conn=db_conn)
137143
ds.set_interdependencies(interdeps)
138144
ds.mark_started()
139145
ds.add_results([{p_x.name: 1.0, p_y.name: 2.0}])
140146
ds.mark_completed()
141147
ds.add_metadata("my_tag", "hello")
142-
ds.conn.close()
143148

144149
# An existing ad-hoc metadata column is returned ...
145-
overview = get_db_overview(db_path, extra_columns=["my_tag"])
150+
overview = get_db_overview(conn=db_conn, extra_columns=["my_tag"])
146151
assert overview[1]["my_tag"] == "hello" # type: ignore[typeddict-item]
147152

148153
# ... while a non-existent column is silently skipped.
149-
overview = get_db_overview(db_path, extra_columns=["does_not_exist"])
154+
overview = get_db_overview(conn=db_conn, extra_columns=["does_not_exist"])
150155
assert "does_not_exist" not in overview[1]
151156

152157

153-
def test_get_db_overview_accepts_connection(tmp_path: Path) -> None:
154-
db_path = str(tmp_path / "conn.db")
155-
_make_db_with_runs(db_path, n_runs=1)
158+
def test_get_db_overview_accepts_connection(db_conn: AtomicConnection) -> None:
159+
_create_runs(db_conn, n_runs=1)
156160

157-
conn = connect(db_path)
158-
try:
159-
overview = get_db_overview(conn=conn)
160-
assert set(overview.keys()) == {1}
161-
# the externally supplied connection must remain usable
162-
assert conn.execute("SELECT COUNT(*) FROM runs").fetchone()[0] == 1
163-
finally:
164-
conn.close()
161+
overview = get_db_overview(conn=db_conn)
162+
assert set(overview.keys()) == {1}
163+
# the externally supplied connection must remain usable
164+
assert db_conn.execute("SELECT COUNT(*) FROM runs").fetchone()[0] == 1
165165

166166

167-
def test_get_db_overview_rejects_conn_and_path(tmp_path: Path) -> None:
168-
db_path = str(tmp_path / "both.db")
169-
_make_db_with_runs(db_path, n_runs=1)
167+
def test_get_db_overview_rejects_conn_and_path(db_conn: AtomicConnection) -> None:
168+
_create_runs(db_conn, n_runs=1)
170169

171-
conn = connect(db_path)
172-
try:
173-
with pytest.raises(ValueError):
174-
get_db_overview(path_to_db=db_path, conn=conn)
175-
finally:
176-
conn.close()
170+
with pytest.raises(ValueError):
171+
get_db_overview(path_to_db=path_to_dbfile(db_conn), conn=db_conn)
177172

178173

179-
def test_get_db_overview_in_progress_uses_live_row_count(tmp_path: Path) -> None:
180-
db_path = str(tmp_path / "in_progress.db")
181-
initialise_or_create_database_at(db_path)
182-
load_or_create_experiment("exp", sample_name="sample")
174+
def test_get_db_overview_in_progress_uses_live_row_count(
175+
db_conn: AtomicConnection,
176+
) -> None:
177+
exp = load_or_create_experiment("exp", sample_name="sample", conn=db_conn)
183178
p_x = ParamSpecBase("x", "numeric")
184179
p_y = ParamSpecBase("y", "numeric")
185180
interdeps = InterDependencies_(dependencies={p_y: (p_x,)})
186181

187182
# run 1: started, still in progress, with 5 data points
188-
ds_live = new_data_set("live")
183+
ds_live = new_data_set("live", exp_id=exp.exp_id, conn=db_conn)
189184
ds_live.set_interdependencies(interdeps)
190185
ds_live.mark_started()
191186
for i in range(5):
192187
ds_live.add_results([{p_x.name: float(i), p_y.name: float(i)}])
193188
# run 2: started, in progress, no data yet
194-
ds_empty = new_data_set("empty")
189+
ds_empty = new_data_set("empty", exp_id=exp.exp_id, conn=db_conn)
195190
ds_empty.set_interdependencies(interdeps)
196191
ds_empty.mark_started()
197-
ds_empty.conn.close() # shared connection for both datasets
198192

199-
overview = get_db_overview(db_path)
193+
overview = get_db_overview(conn=db_conn)
200194

201195
# in-progress runs report the live results-table row count and have no
202196
# completed timestamp
@@ -207,37 +201,29 @@ def test_get_db_overview_in_progress_uses_live_row_count(tmp_path: Path) -> None
207201

208202

209203
def test_get_db_overview_missing_results_table_reports_zero(
210-
tmp_path: Path,
204+
db_conn: AtomicConnection,
211205
) -> None:
212-
db_path = str(tmp_path / "no_table.db")
213-
_make_db_with_runs(db_path, n_runs=1, n_points=5)
206+
_create_runs(db_conn, n_runs=1, n_points=5)
214207

215-
conn = connect(db_path)
216-
try:
217-
(table_name,) = conn.execute(
218-
"SELECT result_table_name FROM runs WHERE run_id=1"
219-
).fetchone()
220-
conn.execute(f'DROP TABLE "{table_name}"')
221-
conn.commit()
222-
finally:
223-
conn.close()
208+
(table_name,) = db_conn.execute(
209+
"SELECT result_table_name FROM runs WHERE run_id=1"
210+
).fetchone()
211+
db_conn.execute(f'DROP TABLE "{table_name}"')
212+
db_conn.commit()
224213

225214
# with the results table gone (and no shape info) the count is unknown;
226215
# ``result_counter`` is the run ordinal, not a data-point count, so it must
227216
# not be used as a fallback
228-
overview = get_db_overview(db_path)
217+
overview = get_db_overview(conn=db_conn)
229218
assert overview[1]["records"] == 0
230219

231220

232-
def test_get_db_overview_query_error_returns_empty(tmp_path: Path) -> None:
233-
db_path = str(tmp_path / "broken.db")
234-
_make_db_with_runs(db_path, n_runs=1)
221+
def test_get_db_overview_query_error_returns_empty(
222+
db_conn: AtomicConnection,
223+
) -> None:
224+
_create_runs(db_conn, n_runs=1)
235225

236-
conn = connect(db_path)
237-
try:
238-
# remove a table the overview query depends on so the JOIN fails
239-
conn.execute("DROP TABLE experiments")
240-
conn.commit()
241-
assert get_db_overview(conn=conn) == {}
242-
finally:
243-
conn.close()
226+
# remove a table the overview query depends on so the JOIN fails
227+
db_conn.execute("DROP TABLE experiments")
228+
db_conn.commit()
229+
assert get_db_overview(conn=db_conn) == {}

0 commit comments

Comments
 (0)