From 08c89654010783aeabe09d2862b2cc085aa215a0 Mon Sep 17 00:00:00 2001 From: srujan-rai Date: Mon, 27 Apr 2026 23:08:16 +0530 Subject: [PATCH 1/2] fix(bigquery): handle API-wrapped null values in NULLABLE fields BigQuery returns nulls as {'v': None}, not bare None. The old check `value is None` never matched, causing convert(None) to raise TypeError (eg. int(None)) for NULLABLE fields. Use flatten(value) before the check. Adds regression test for the {'v': None} API response shape. --- bigquery/gcloud/aio/bigquery/utils.py | 4 ++-- bigquery/tests/unit/utils_test.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bigquery/gcloud/aio/bigquery/utils.py b/bigquery/gcloud/aio/bigquery/utils.py index 0216384d4..37bc69d8f 100644 --- a/bigquery/gcloud/aio/bigquery/utils.py +++ b/bigquery/gcloud/aio/bigquery/utils.py @@ -101,8 +101,8 @@ def parse(field: dict[str, Any], value: Any) -> Any: ) raise - if field['mode'] == 'NULLABLE' and value is None: - return value + if field['mode'] == 'NULLABLE' and flatten(value) is None: + return None if field['mode'] == 'REPEATED': if field['type'] == 'RECORD': diff --git a/bigquery/tests/unit/utils_test.py b/bigquery/tests/unit/utils_test.py index fdd158009..3320c1c9d 100644 --- a/bigquery/tests/unit/utils_test.py +++ b/bigquery/tests/unit/utils_test.py @@ -105,6 +105,8 @@ def test_parse_nullable(kind): # make sure we never convert to a falsey typed equivalent # eg. for BOOLEAN, None != False assert parse(field, None) is None + # BigQuery API wraps null values as {'v': None} -- must not crash + assert parse(field, {'v': None}) is None @pytest.mark.parametrize( From 6defbb6317e11d486123e5a376ab2aca9a097ed9 Mon Sep 17 00:00:00 2001 From: srujan-rai Date: Mon, 27 Apr 2026 23:15:47 +0530 Subject: [PATCH 2/2] refactor(bigquery): flatten value once at top of parse() Addresses review feedback: flatten value once before all mode/type checks instead of calling flatten(value) redundantly on every branch. --- bigquery/gcloud/aio/bigquery/utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bigquery/gcloud/aio/bigquery/utils.py b/bigquery/gcloud/aio/bigquery/utils.py index 37bc69d8f..118346301 100644 --- a/bigquery/gcloud/aio/bigquery/utils.py +++ b/bigquery/gcloud/aio/bigquery/utils.py @@ -101,7 +101,9 @@ def parse(field: dict[str, Any], value: Any) -> Any: ) raise - if field['mode'] == 'NULLABLE' and flatten(value) is None: + value = flatten(value) + + if field['mode'] == 'NULLABLE' and value is None: return None if field['mode'] == 'REPEATED': @@ -110,17 +112,17 @@ def parse(field: dict[str, Any], value: Any) -> Any: f['name']: parse(f, x) for f, x in zip(field['fields'], xs) } - for xs in flatten(value)] + for xs in value] - return [convert(x) for x in flatten(value)] + return [convert(x) for x in value] if field['type'] == 'RECORD': return { f['name']: parse(f, x) - for f, x in zip(field['fields'], flatten(value)) + for f, x in zip(field['fields'], value) } - return convert(flatten(value)) + return convert(value) def query_response_to_dict(response: dict[str, Any]) -> list[dict[str, Any]]: