feat: split raw data storage into per-dataset SQLite files - #8219
feat: split raw data storage into per-dataset SQLite files#8219astafan8 wants to merge 23 commits into
Conversation
Add optional configuration to write results-table data into individual
per-dataset SQLite files (<guid>.db) while keeping all metadata in the
main database. This keeps the main DB lightweight as it grows.
Config options (dataset section of qcodesrc.json):
- raw_data_to_separate_db (bool, default false)
- raw_data_path (string, default '{db_location}')
Implementation:
- New module: qcodes.dataset.raw_data_storage (helper functions)
- DataSet._data_conn property routes reads/writes to correct DB
- BackgroundWriter supports per-dataset raw data connections
- Subscriber triggers created on data connection for compatibility
- Per-dataset DB path persisted in run metadata for auto-reconnect
- Empty results table schema kept in main DB for compatibility
- 19 new tests, all existing tests pass unchanged
- Documentation added to dataset intro, design docs, and Database notebook
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #8219 +/- ##
==========================================
+ Coverage 71.11% 71.31% +0.20%
==========================================
Files 305 306 +1
Lines 32004 32308 +304
==========================================
+ Hits 22760 23042 +282
- Misses 9244 9266 +22 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
- Rename raw_data_storage.py to _raw_data_storage.py (private module) - Raise FileNotFoundError when per-dataset raw data file is missing on load instead of silently falling back to empty main DB table - Quote column names in create_raw_data_db to handle SQL keyword names - Close both _raw_data_conn and conn in all tests to prevent leaked file handles - Add test_missing_raw_data_file_raises to verify the error behavior - Fix import ordering (ruff I001) and raw regex pattern (ruff RUF043) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement a helper function that updates the raw_data_db_path metadata in the main database when individual raw data SQLite files have been moved to a new location. This mirrors the existing pattern used for exported netCDF files. The function: - Scans all datasets in the main DB that have raw_data_db_path metadata - For each, checks if the corresponding .db file exists in the new folder - Updates the stored path in the metadata to point to the new location - Reports which datasets were updated and which were skipped (file missing) Exposed as qcodes.dataset.update_raw_data_paths() for user convenience. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Added: update_raw_data_paths helperAdded a utility function for users who move their per-dataset raw data files to a new location. This mirrors the pattern used for exported netCDF files. Usage: `python update_raw_data_paths( The function:
4 tests added, documentation updated in introduction.rst and Database.ipynb. |
- Apply ruff format to _raw_data_storage.py (line length adjustments) - Comment out unused import in Database.ipynb example cell Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add assert statements to narrow path_to_db from str | None to str before passing to update_raw_data_paths(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two new functions for managing per-dataset raw data storage: - purge_orphaned_datasets(): Finds and removes dataset records from the main DB whose raw data files no longer exist on disk. Useful after archiving/deleting selected raw data files. Defaults to dry_run=True. - cleanup_datasets(): Removes datasets (both DB records AND raw data files) matching criteria: older_than_days, sample_name (exact match), or larger_than_mb. Criteria use AND logic. Defaults to dry_run=True. Both functions return dataclass results with full details of what was found/removed. The _remove_dataset_from_db helper correctly deletes from runs, layouts, dependencies tables and drops the results table. Documentation added to introduction.rst and Database.ipynb. 9 new tests covering both functions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Change per-dataset log.info to log.debug in purge/cleanup/update_paths - Replace os.remove with Path.unlink() for robustness - Remove unused os import Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- _extract_single_dataset_into_db: use dataset._data_conn instead of dataset.conn so raw data is read from the per-dataset file - unsubscribe/unsubscribe_all: use _data_conn to remove triggers that were created on the raw data connection - Add tests for extract_runs_into_db and export_datasets_and_create_metadata_db with split raw data Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… prints, fix docs Co-authored-by: astafan8 <15662810+astafan8@users.noreply.github.com>
|
|
||
| Unlike the main QCoDeS :func:`~qcodes.dataset.sqlite.database.connect`, | ||
| this does **not** create the full metadata schema (experiments, runs, ...). | ||
| It only registers the numpy/sqlite type adapters that QCoDeS needs to |
There was a problem hiding this comment.
ok, so instead of copying this over here from connect funcition, can't you extract that part into a function and re-use it in here and in connect? the same is true for the rest of this function
There was a problem hiding this comment.
Addressed in ae3c879. Extracted the shared logic into connect_to_sqlite_file() and _register_numpy_sqlite_adapters_and_converters() in sqlite/database.py, and now both connect() and connect_to_raw_data_db() reuse them. I also reused the existing _create_run_table() in create_raw_data_db() (instead of duplicating the results-table CREATE TABLE logic), and reused _validate_table_name() before the DROP TABLE in remove_dataset_from_db().
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Mikhail Astafev <astafan8@gmail.com>
- Extract connect_to_sqlite_file() and _register_numpy_sqlite_adapters_and_converters() in sqlite/database.py; use them in both connect() and connect_to_raw_data_db() so the numpy adapters and connection settings are defined once - Reuse _create_run_table() in create_raw_data_db() instead of duplicating the results-table CREATE TABLE logic (also gains table-name validation) - Validate table name via _validate_table_name() before DROP TABLE in remove_dataset_from_db() - Apply doc review suggestions and fix module path in dataset_design.rst Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
|
||
| Unlike :func:`connect`, this does **not** create or upgrade any QCoDeS | ||
| schema; it is the low-level building block shared by :func:`connect` and | ||
| the per-dataset raw-data connection helper. |
There was a problem hiding this comment.
Then perhaps it should be private?
There was a problem hiding this comment.
Agreed - made it private (_connect_to_sqlite_file) in 198f919. It's only used internally by connect() and connect_to_raw_data_db().
|
|
||
| def get_datasets_with_raw_data_path( | ||
| conn: AtomicConnection, | ||
| ) -> list[tuple[int, str, str, str, float | None, float | None, str, str]]: |
There was a problem hiding this comment.
replace this type with a named tuple or dataclass?
There was a problem hiding this comment.
Done in 198f919. Now returns a RawDataDatasetRecord dataclass instead of the opaque tuple.
There was a problem hiding this comment.
@astafan8 That change never seems to have made it to this branch
Previously a results table was still created (empty) in the main database for split-storage datasets, purely to satisfy code paths that assumed the table exists. This removes that table entirely so the main DB holds only metadata - cleaner and future-proof for non-sqlite raw data backends. Changes: - Do not create the results table in the main DB when raw storage is enabled (create_run_table=False, insert_into_results_table=False), mirroring how DataSetInMem records runs. - Identify split-storage runs via a dedicated 'raw_data_db_path' column in the runs table, recorded at dataset creation. This disambiguates them from DataSetInMem runs (which also have no results table) in _get_datasetprotocol_from_guid, replacing the old reliance on the empty table's presence. - Keep 'raw_data_db_path' out of the user-facing metadata dict (read/write via get/set_raw_data_db_path_for_run); this also stops it leaking into metadata comparisons and extract/export copies. - number_of_results/__len__ return 0 when the results table does not exist (e.g. a pristine, not-yet-started split dataset). - Defer subscriptions requested before the dataset is started until start time, when the raw data table exists (fixes subscriber trigger creation). - Restrict management queries (purge/cleanup) to started runs only. - Docs + tests updated; add tests for subscribe-before-start, count before start, no-main-table, and extract not carrying over raw_data_db_path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
and idea: why don't we leverage the export-info infra, add another export type qcodes-raw-sqlite-db with .db extension isntead of adding a new raw_data_db_path column. what would be the pros/cons of that? |
|
an idea: should this implementation be part of DataSet class or should we add DataSetInSeparateSqliteDbFile class, or a DataSetInSeparateFile class with sqlite-db implementation and a place for further storage implementations? what would be pros-cons of those? |
Address review: factor the subscriber creation into _create_and_start_subscriber (reused by subscribe and _start_pending_subscribers) and the pending-subscription bookkeeping into _queue_pending_subscriber. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-table Avoid empty results table in main DB for split raw data storage
…s for query result - Rename connect_to_sqlite_file -> _connect_to_sqlite_file (internal helper, not part of the public API) - Return a RawDataDatasetRecord dataclass from get_datasets_with_raw_data_path instead of an opaque 8-tuple Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Re: reusing the export-info infra (a I researched this and I'd recommend against it. Findings:
Net: the dedicated, metadata-excluded column is the cleaner model because raw-sqlite storage is a live backend, not an export. If we ever want a unified "where does this dataset's data live" registry, I'd argue for a small purpose-built abstraction rather than overloading |
|
Re: should this live in I agree the config-flag-in- Design:
This keeps I'll open the stacked PR against this branch shortly. |
Summary
This PR implements split raw data storage for QCoDeS: an opt-in feature that writes raw measurement data (results table rows) into individual per-dataset SQLite files while keeping all metadata in the main database. The goal is to prevent the main DB file from growing excessively large as datasets accumulate, so that a user can keep a single lightweight metadata database on their machine instead of many ever-growing ones.
It also includes dataset management helpers for maintaining the main database and raw data files over time.
Motivation
The main QCoDeS SQLite database stores both metadata (experiments, runs, parameter layouts, dependencies) and raw measurement data (results tables) in a single file. Over time this file can grow to many gigabytes, slowing down operations that only need metadata. By splitting the raw data into per-dataset files, the main DB stays lightweight while data integrity is preserved.
Design
Transparent routing via
_data_conn_data_connproperty onDataSetis the routing point for all results-data read/write operations. It returns the per-dataset raw data connection when split storage is enabled, otherwise the main DB connection.add_results,_BackgroundWriter) and read paths (get_parameter_data,DataSetCacheWithDBBackend,number_of_results,__len__) go through it.Config — follows the existing export-path pattern
Two new options in the
datasetsection ofqcodesrc.json:raw_data_to_separate_db(bool, defaultfalse) — enables split storage.raw_data_path(string, default"{db_location}") — folder for per-dataset files.{db_location}expands to a folder derived from the main DB path (reusing_expand_export_path).Per-dataset files — lightweight, GUID-named
<guid>.dband contains only the results table + numpy type adapters — no QCoDeS metadata schema._create_run_table()/_connect_to_sqlite_file()helpers so the schema and connection settings stay identical to the main DB.No empty results table in the main DB
DataSetInMem(the netcdf-backed dataset) records runs (create_run_table=False).raw_data_db_pathcolumn in therunstable, recorded at dataset creation. This positively distinguishes them fromDataSetInMemruns (which also have no results table) in_get_datasetprotocol_from_guid, and is future-proof for non-SQLite raw-data backends.raw_data_db_pathis an internal storage detail: it is stored in therunstable but excluded from the user-facingmetadatadict, and read/written throughget_raw_data_db_path_for_run/set_raw_data_db_path_for_run. This keeps it out of metadata comparisons (the_same_dataset_as) and out of extracted/exported databases.Features that depend on the results table — handled explicitly
number_of_results/__len__) return0when the results table does not exist (e.g. a pristine, not-yet-started split dataset)._queue_pending_subscriber/_create_and_start_subscriber/_start_pending_subscribers.unsubscribe,unsubscribe_alloperate on_data_connso they target the DB that actually holds the results table.Extract / export compatibility
extract_runs_into_dbreads results via_data_conn(from the per-dataset file), and the copied run does not carry over the source'sraw_data_db_path.export_datasets_and_create_metadata_dbworks: the NetCDF path goes throughget_parameter_data(already routed), and the copy-as-is fallback uses the extract fix above.Shared connection helpers (deduplication)
_connect_to_sqlite_file()and_register_numpy_sqlite_adapters_and_converters()insqlite/database.py; bothconnect()andconnect_to_raw_data_db()reuse them, so the numpy adapters and connection settings are defined once.Dataset management helpers
Exposed via
qcodes.dataset:update_raw_data_paths(db_path, new_raw_data_folder)— update stored paths after per-dataset files have been moved to a new folder.purge_orphaned_datasets(db_path, *, dry_run=True)— remove dataset records from the main DB whose raw data files no longer exist on disk.cleanup_datasets(db_path, *, older_than_days=None, sample_name=None, larger_than_mb=None, dry_run=True)— remove datasets (DB records AND raw data files) matching criteria (AND logic).Both purge and cleanup default to
dry_run=Trueand return dataclass results with full details. Dataset removal reuses the existingremove_dataset_from_db/_validate_table_namemachinery.Files changed
src/qcodes/dataset/_raw_data_storage.pycreate_raw_data_db(),connect_to_raw_data_db(),update_raw_data_paths(),purge_orphaned_datasets(),cleanup_datasets(), result dataclassessrc/qcodes/dataset/data_set.py_data_conn/_results_table_existsproperties,_raw_data_db_path, routing in__init__,_perform_start_actions,add_results,get_parameter_data,number_of_results,__len__,_BackgroundWriter, deferred subscribers,unsubscribe/unsubscribe_all, split-vs-inmem disambiguationsrc/qcodes/dataset/sqlite/queries.pyget_raw_data_db_path_for_run/set_raw_data_db_path_for_run,RawDataDatasetRecorddataclass +get_datasets_with_raw_data_path,remove_dataset_from_db,RAW_DATA_DB_PATH_COLUMNexcluded from metadatasrc/qcodes/dataset/sqlite/database.py_connect_to_sqlite_file()+_register_numpy_sqlite_adapters_and_converters()reused byconnect()src/qcodes/dataset/database_extract_runs.py_extract_single_dataset_into_dbreads via_data_connsrc/qcodes/dataset/data_set_cache.pyload_data_from_db()uses_data_connsrc/qcodes/dataset/subscriber.py_data_connsrc/qcodes/dataset/__init__.pysrc/qcodes/configuration/qcodesrc.json+qcodesrc_schema.jsondocs/dataset/introduction.rst,docs/dataset/dataset_design.rst,docs/examples/DataSet/Database.ipynbdocs/changes/newsfragments/8219.newtests/dataset/test_raw_data_storage.pyVerification
tests/dataset/test_raw_data_storage.py: all pass (unit, integration, management, extract/export, subscribe-before-start, count-before-start, no-main-table).tests/datasetsuite in default mode: no regressions.queries.get_parameter_data(ds.conn, ...)directly bypassing the DataSet API, and db-overview/guid-scan tests that inherently assume data in the main DB).