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 16 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.29% +0.18%
==========================================
Files 305 306 +1
Lines 32004 32271 +267
==========================================
+ Hits 22760 23009 +249
- Misses 9244 9262 +18 ☔ 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>
…implify - Move _get_datasets_with_raw_data and _remove_dataset_from_db to src/qcodes/dataset/sqlite/queries.py as public functions - Add print() statements alongside logging for user visibility - Use 'with closing(conn):' pattern for connection management - Simplify orphan detection (rely on raw_data_size_bytes is None) - Simplify cleanup_datasets docstring - Simplify introduction.rst to reference Database notebook - Update news fragment with config option name and DB context 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>
| ====================== | ||
|
|
||
| As the main SQLite database grows with many datasets, browsing experiments and | ||
| loading metadata can become slower due to the file size. To address this, |
There was a problem hiding this comment.
| loading metadata can become slower due to the file size. To address this, | |
| managing the database file can become inconvenient due to the file size. To address this, |
|
|
||
| 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
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Mikhail Astafev <astafan8@gmail.com>
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, making metadata browsing and experiment management faster.
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 Decisions
Architecture: transparent routing via
_data_connproperty_data_connproperty onDataSetis the routing point for all data read/write operationsself.conn(main DB)add_results,_BackgroundWriter) and read paths (get_parameter_data,DataSetCacheWithDBBackend,number_of_results,__len__) go through this propertyConfig: follows existing export path pattern
datasetsection ofqcodesrc.json:raw_data_to_separate_db(bool, defaultfalse)raw_data_path(string, default"{db_location}")_expand_export_path()fromexport_config.pyfor path expansion (e.g.,~/experiments.db->~/experiments_db/)export_path/export_typeconfig approachPer-dataset files: lightweight, GUID-named
<guid>.dband contains only the results table + numpy type adaptersraw_data_db_pathdynamic column) for automatic reconnection onload_by_id()Empty results table kept in main DB
_Subscribertrigger creation (SQLite triggers require the table to exist)__len__/number_of_resultsbefore dataset is started (when raw data DB doesn't yet exist)PRAGMA TABLE_INFO_check_if_table_foundlogic used in_get_datasetprotocol_from_guidto distinguish DataSet vs DataSetInMemget_parameter_databypass for raw data DBget_parameter_data()inqueries.pycallsget_rundescriber_from_result_table_name()which queries therunstable — this table doesn't exist in the raw data DB_raw_data_connis set, bypass the top-level function and callget_shaped_parameter_data_for_one_paramtree()directly with the already-held rundescriberSubscriber triggers on data connection
_Subscriber.__init__creates SQL triggers for real-time data callbacks_data_conninstead ofself.connso triggers fire on the correct DB where data is actually insertedBackgroundWriter support
_BackgroundWritermaintains a_raw_data_connsdict keyed by file pathraw_data_pathkey for routingDataset Management Helpers
Three management functions are exposed via
qcodes.dataset:update_raw_data_paths(db_path, new_raw_data_folder)Updates stored paths in the main database after raw data files have been moved to a new folder. Scans all datasets with a
raw_data_db_pathmetadata entry, checks if the corresponding.dbfile exists in the new folder, and updates the path.purge_orphaned_datasets(db_path, *, dry_run=True)Finds and removes dataset records from the main DB whose raw data files no longer exist on disk. Use case: after users archive or delete selected raw data files, this cleans up stale references. Defaults to
dry_run=True— first see what would be removed, then setdry_run=Falseto execute.cleanup_datasets(db_path, *, older_than_days=None, sample_name=None, larger_than_mb=None, dry_run=True)Removes datasets (both DB records AND raw data files on disk) matching specified criteria. Criteria use AND logic. Use cases:
Defaults to
dry_run=True. Both purge and cleanup return dataclass results with full details.Compatibility with existing features (extract/export)
Audited all functions that read raw measurement data to ensure they work correctly when data is in separate per-dataset SQLite files:
extract_runs_into_db: Fixed to use_data_conninstead ofconnso raw data is read from the per-dataset file (not the empty results table in the main DB)export_datasets_and_create_metadata_db: NetCDF export path works correctly (goes throughget_parameter_datawhich already routes via_data_conn); the_copy_dataset_as_isfallback path is fixed by the extract fix aboveunsubscribe/unsubscribe_all: Fixed to use_data_connto remove triggers that were created on the raw data connectionget_parameter_data,to_pandas_*,to_xarray_*,export, cache, plotting: All work correctly — they go through_data_connorget_parameter_data()which already routes properlyextract_runs_into_dbandexport_datasets_and_create_metadata_dbwith split storage enabledFiles 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()src/qcodes/dataset/sqlite/queries.pyget_datasets_with_raw_data_path(),remove_dataset_from_db()tests/dataset/test_raw_data_storage.pysrc/qcodes/dataset/data_set.py_data_connproperty,_raw_data_connattribute, routing in__init__,_perform_start_actions,add_results,get_parameter_data,number_of_results,__len__,_BackgroundWriter,unsubscribe/unsubscribe_allsrc/qcodes/dataset/database_extract_runs.py_extract_single_dataset_into_dbuses_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.jsonsrc/qcodes/configuration/qcodesrc_schema.jsondocs/dataset/introduction.rstdocs/dataset/dataset_design.rstdocs/examples/DataSet/Database.ipynbdocs/changes/newsfragments/8219.newVerification
Own tests: 35/35 pass
load_by_id, multiple datasets, background writerextract_runs_into_dbandexport_datasets_and_create_metadata_dbwith split dataFull test suite with feature globally enabled: 1031 passed, 5 failed, 35 skipped
The 5 failures are all expected/explained:
test_raw_data_conn_is_nonetest_data_in_main_dbtest_get_parameter_dataqueries.get_parameter_data(ds.conn, ...)directly, bypassing DataSettest_get_parameter_data_independent_parameterstest_get_run_attributes{'foo': 'bar'}but split addsraw_data_db_pathFull test suite with feature disabled (default): all pass unchanged
Code quality