From 5c5a9333ebf6edec47744cb891b84226a79df9e8 Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Mon, 21 Sep 2026 10:25:40 +0530 Subject: [PATCH 1/3] Fail meaningfully on metadata a column cannot hold Adding metadata whose value is a nested dict or a sequence surfaced as a bare sqlite3 binding error wrapped in the generic rollback RuntimeError: sqlite3.ProgrammingError: Error binding parameter 1: type 'list' is not supported which names neither the tag nor what to do about it. validate_dynamic_column_data already rejects invalid tags and None with a clear message, so extend it to reject values SQLite cannot store. The check asks SQLite itself whether the value binds rather than comparing against a list of types, so values NumPy registers an adapter for -- scalars and arrays -- keep working as before. Fixes #1444 Co-Authored-By: Claude Opus 5 --- src/qcodes/dataset/sqlite/queries.py | 32 ++++++++++++++++++++++++++++ tests/dataset/test_dataset_basic.py | 17 +++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/qcodes/dataset/sqlite/queries.py b/src/qcodes/dataset/sqlite/queries.py index c42eeb676f1..c0c96d9548c 100644 --- a/src/qcodes/dataset/sqlite/queries.py +++ b/src/qcodes/dataset/sqlite/queries.py @@ -5,6 +5,7 @@ from __future__ import annotations +import contextlib import datetime import logging import sqlite3 @@ -1865,6 +1866,24 @@ def get_metadata_from_run_id(conn: AtomicConnection, run_id: int) -> dict[str, A return metadata +def _is_storable_in_column(val: Any) -> bool: + """ + Return whether SQLite can store ``val`` in a column. + + Rather than comparing against a fixed list of types, this asks SQLite + itself whether it can bind the value, so any type with a registered + adapter -- NumPy scalars and arrays, for instance -- is still accepted. + The statement is a bare ``SELECT`` against a throwaway in-memory + connection, so nothing is written anywhere. + """ + with contextlib.closing(sqlite3.connect(":memory:")) as probe: + try: + probe.execute("SELECT ?", (val,)) + except (sqlite3.InterfaceError, sqlite3.ProgrammingError): + return False + return True + + def validate_dynamic_column_data(data: Mapping[str, Any]) -> None: """ Validate the given dicts tags and values. Note that None is not a valid @@ -1874,6 +1893,12 @@ def validate_dynamic_column_data(data: Mapping[str, Any]) -> None: Args: data: the metadata mapping (tags to values) + Raises: + KeyError: if a tag is not a valid SQLite column name. + ValueError: if a value is None. + TypeError: if a value cannot be stored in a SQLite column, such as a + nested dict or a sequence. + """ for tag, val in data.items(): if not tag.isidentifier(): @@ -1885,6 +1910,13 @@ def validate_dynamic_column_data(data: Mapping[str, Any]) -> None: raise ValueError( f"Tag {tag} has value None. That is not a valid metadata value!" ) + if not _is_storable_in_column(val): + raise TypeError( + f"Tag {tag} has value of type {type(val).__name__}. That is " + "not a valid metadata value: a column stores a single SQLite " + "value, so a nested dict or a sequence has to be serialized " + "first, for example with json.dumps." + ) def insert_data_in_dynamic_columns( diff --git a/tests/dataset/test_dataset_basic.py b/tests/dataset/test_dataset_basic.py index bd3e6ae77a0..1b35782d8ca 100644 --- a/tests/dataset/test_dataset_basic.py +++ b/tests/dataset/test_dataset_basic.py @@ -785,6 +785,23 @@ def test_metadata(experiment, request: FixtureRequest) -> None: ds1.add_metadata(good_tag, None) assert error_caused_by(e2, none_value_msg) + # A column holds a single SQLite value, so anything nested has to say so + # rather than surfacing as a bare sqlite3 binding error. See issue #1444. + for bad_value in ({"b": 1}, [1, 2], (1, 2), {1, 2}): + nested_value_msg = ( + f"Tag {good_tag} has value of type {type(bad_value).__name__}. " + "That is not a valid metadata value" + ) + with pytest.raises( + RuntimeError, match="Rolling back due to unhandled exception" + ) as e3: + ds1.add_metadata(good_tag, bad_value) + assert error_caused_by(e3, nested_value_msg) + + # Values NumPy registers an adapter for must keep working. + ds1.add_metadata("np_scalar", np.int64(3)) + assert ds1.metadata["np_scalar"] == 3 + def test_the_same_dataset_as(some_interdeps, experiment) -> None: ds = DataSet() From 2e65ad3852405e9ef7c084ef4567e95842e8e5b2 Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Mon, 21 Sep 2026 10:26:23 +0530 Subject: [PATCH 2/3] Add newsfragment Co-Authored-By: Claude Opus 5 --- docs/changes/newsfragments/8518.improved | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/changes/newsfragments/8518.improved diff --git a/docs/changes/newsfragments/8518.improved b/docs/changes/newsfragments/8518.improved new file mode 100644 index 00000000000..6adc0ad4c36 --- /dev/null +++ b/docs/changes/newsfragments/8518.improved @@ -0,0 +1,6 @@ +Adding metadata that SQLite cannot store in a column now fails with a message naming the tag and its type. + +A nested dict or a sequence previously surfaced as ``sqlite3.ProgrammingError: Error binding parameter 1: +type 'list' is not supported``, which said nothing about which tag was at fault. ``validate_dynamic_column_data`` +now rejects such values with a ``TypeError`` suggesting serialization, alongside its existing checks for invalid +tags and ``None``. Values that NumPy registers a SQLite adapter for, such as scalars and arrays, are unaffected. From 9880d1fc8bcde47597253cf0f9b3e7bd94292dd8 Mon Sep 17 00:00:00 2001 From: Mikhail Astafev Date: Mon, 28 Sep 2026 10:33:24 +0200 Subject: [PATCH 3/3] Update src/qcodes/dataset/sqlite/queries.py --- src/qcodes/dataset/sqlite/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qcodes/dataset/sqlite/queries.py b/src/qcodes/dataset/sqlite/queries.py index c0c96d9548c..5f9999c2860 100644 --- a/src/qcodes/dataset/sqlite/queries.py +++ b/src/qcodes/dataset/sqlite/queries.py @@ -1913,7 +1913,7 @@ def validate_dynamic_column_data(data: Mapping[str, Any]) -> None: if not _is_storable_in_column(val): raise TypeError( f"Tag {tag} has value of type {type(val).__name__}. That is " - "not a valid metadata value: a column stores a single SQLite " + "not a valid metadata value. Note that a column stores a single SQLite " "value, so a nested dict or a sequence has to be serialized " "first, for example with json.dumps." )