diff --git a/bigquery/google/cloud/bigquery/_helpers.py b/bigquery/google/cloud/bigquery/_helpers.py index 98eadb0a2f8e..21a8e3636d24 100644 --- a/bigquery/google/cloud/bigquery/_helpers.py +++ b/bigquery/google/cloud/bigquery/_helpers.py @@ -424,11 +424,12 @@ def _record_field_to_json(fields, row_value): for subindex, subfield in enumerate(fields): subname = subfield.name - if isdict: - subvalue = row_value.get(subname) - else: - subvalue = row_value[subindex] - record[subname] = _field_to_json(subfield, subvalue) + subvalue = row_value.get(subname) if isdict else row_value[subindex] + + # None values are unconditionally omitted + if subvalue is not None: + record[subname] = _field_to_json(subfield, subvalue) + return record diff --git a/bigquery/tests/unit/test__helpers.py b/bigquery/tests/unit/test__helpers.py index 6d92b4de73ba..fa6d27c981d8 100644 --- a/bigquery/tests/unit/test__helpers.py +++ b/bigquery/tests/unit/test__helpers.py @@ -856,14 +856,39 @@ def test_w_non_empty_dict(self): converted = self._call_fut(fields, original) self.assertEqual(converted, {"one": "42", "two": "two"}) - def test_w_missing_nullable(self): + def test_w_some_missing_nullables(self): fields = [ _make_field("INT64", name="one", mode="NULLABLE"), _make_field("STRING", name="two", mode="NULLABLE"), ] original = {"one": 42} converted = self._call_fut(fields, original) - self.assertEqual(converted, {"one": "42", "two": None}) + + # missing fields should not be converted to an explicit None + self.assertEqual(converted, {"one": "42"}) + + def test_w_all_missing_nullables(self): + fields = [ + _make_field("INT64", name="one", mode="NULLABLE"), + _make_field("STRING", name="two", mode="NULLABLE"), + ] + original = {} + converted = self._call_fut(fields, original) + + # we should get an empty dict, not None + self.assertEqual(converted, {}) + + def test_w_explicit_none_value(self): + fields = [ + _make_field("INT64", name="one", mode="NULLABLE"), + _make_field("STRING", name="two", mode="NULLABLE"), + _make_field("BOOL", name="three", mode="REPEATED"), + ] + original = {"three": None, "one": 42, "two": None} + converted = self._call_fut(fields, original) + + # None values should be dropped regardless of the field type + self.assertEqual(converted, {"one": "42"}) class Test_field_to_json(unittest.TestCase): diff --git a/bigquery/tests/unit/test_client.py b/bigquery/tests/unit/test_client.py index cce4bc532074..f20e774d0867 100644 --- a/bigquery/tests/unit/test_client.py +++ b/bigquery/tests/unit/test_client.py @@ -4636,10 +4636,13 @@ def test_insert_rows_w_schema(self): ] def _row_data(row): + result = {"full_name": row[0], "age": str(row[1])} joined = row[2] - if isinstance(row[2], datetime.datetime): + if isinstance(joined, datetime.datetime): joined = _microseconds_from_datetime(joined) * 1e-6 - return {"full_name": row[0], "age": str(row[1]), "joined": joined} + if joined is not None: + result["joined"] = joined + return result SENT = { "rows": [ @@ -4708,7 +4711,10 @@ def test_insert_rows_w_list_of_dictionaries(self): def _row_data(row): joined = row["joined"] - if isinstance(joined, datetime.datetime): + if joined is None: + row = copy.deepcopy(row) + del row["joined"] + elif isinstance(joined, datetime.datetime): row["joined"] = _microseconds_from_datetime(joined) * 1e-6 row["age"] = str(row["age"]) return row @@ -4927,9 +4933,8 @@ def test_insert_rows_w_repeated_fields(self): }, { "json": { - "color": None, "items": [], - "structs": [{"score": None, "times": [], "distances": [3.5]}], + "structs": [{"times": [], "distances": [3.5]}], }, "insertId": "1", }, @@ -4996,10 +5001,7 @@ def test_insert_rows_w_record_schema(self): }, "insertId": "1", }, - { - "json": {"full_name": "Wylma Phlyntstone", "phone": None}, - "insertId": "2", - }, + {"json": {"full_name": "Wylma Phlyntstone"}, "insertId": "2"}, ] }