Skip to content

Commit 23eb8eb

Browse files
authored
Merge branch 'main' into pangea-v1alpha
2 parents bb5c06c + aaf1eb8 commit 23eb8eb

File tree

7 files changed

+153
-114
lines changed

7 files changed

+153
-114
lines changed

.github/.OwlBot.lock.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@
1313
# limitations under the License.
1414
docker:
1515
image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest
16-
digest: sha256:2ed982f884312e4883e01b5ab8af8b6935f0216a5a2d82928d273081fc3be562
17-
# created: 2024-11-12T12:09:45.821174897Z
16+
digest: sha256:8e3e7e18255c22d1489258d0374c901c01f9c4fd77a12088670cd73d580aa737
17+
# created: 2024-12-17T00:59:58.625514486Z

.kokoro/docker/docs/requirements.txt

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
# This file is autogenerated by pip-compile with Python 3.10
33
# by the following command:
44
#
5-
# pip-compile --allow-unsafe --generate-hashes requirements.in
5+
# pip-compile --allow-unsafe --generate-hashes synthtool/gcp/templates/python_library/.kokoro/docker/docs/requirements.in
66
#
7-
argcomplete==3.5.1 \
8-
--hash=sha256:1a1d148bdaa3e3b93454900163403df41448a248af01b6e849edc5ac08e6c363 \
9-
--hash=sha256:eb1ee355aa2557bd3d0145de7b06b2a45b0ce461e1e7813f5d066039ab4177b4
7+
argcomplete==3.5.2 \
8+
--hash=sha256:036d020d79048a5d525bc63880d7a4b8d1668566b8a76daf1144c0bbe0f63472 \
9+
--hash=sha256:23146ed7ac4403b70bd6026402468942ceba34a6732255b9edf5b7354f68a6bb
1010
# via nox
1111
colorlog==6.9.0 \
1212
--hash=sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff \
@@ -23,7 +23,7 @@ filelock==3.16.1 \
2323
nox==2024.10.9 \
2424
--hash=sha256:1d36f309a0a2a853e9bccb76bbef6bb118ba92fa92674d15604ca99adeb29eab \
2525
--hash=sha256:7aa9dc8d1c27e9f45ab046ffd1c3b2c4f7c91755304769df231308849ebded95
26-
# via -r requirements.in
26+
# via -r synthtool/gcp/templates/python_library/.kokoro/docker/docs/requirements.in
2727
packaging==24.2 \
2828
--hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \
2929
--hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f
@@ -32,11 +32,41 @@ platformdirs==4.3.6 \
3232
--hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \
3333
--hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb
3434
# via virtualenv
35-
tomli==2.0.2 \
36-
--hash=sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38 \
37-
--hash=sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed
35+
tomli==2.2.1 \
36+
--hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \
37+
--hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \
38+
--hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \
39+
--hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \
40+
--hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \
41+
--hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \
42+
--hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \
43+
--hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \
44+
--hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \
45+
--hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \
46+
--hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \
47+
--hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \
48+
--hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \
49+
--hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \
50+
--hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \
51+
--hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \
52+
--hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \
53+
--hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \
54+
--hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \
55+
--hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \
56+
--hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \
57+
--hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \
58+
--hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \
59+
--hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \
60+
--hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \
61+
--hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \
62+
--hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \
63+
--hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \
64+
--hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \
65+
--hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \
66+
--hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \
67+
--hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7
3868
# via nox
39-
virtualenv==20.27.1 \
40-
--hash=sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba \
41-
--hash=sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4
69+
virtualenv==20.28.0 \
70+
--hash=sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0 \
71+
--hash=sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa
4272
# via nox

.kokoro/requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,9 @@ jeepney==0.8.0 \
254254
# via
255255
# keyring
256256
# secretstorage
257-
jinja2==3.1.4 \
258-
--hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \
259-
--hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d
257+
jinja2==3.1.5 \
258+
--hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \
259+
--hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb
260260
# via gcp-releasetool
261261
keyring==25.4.1 \
262262
--hash=sha256:5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf \

google/cloud/bigquery/schema.py

Lines changed: 25 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
import collections
2020
import copy
2121
import enum
22-
from typing import Any, Dict, Iterable, Optional, Union, cast
22+
from typing import Any, cast, Dict, Iterable, Optional, Union
2323

24+
from google.cloud.bigquery import _helpers
2425
from google.cloud.bigquery import standard_sql
2526
from google.cloud.bigquery._helpers import (
2627
_isinstance_or_raise,
@@ -240,32 +241,9 @@ def __init__(
240241
self._properties["rangeElementType"] = {"type": range_element_type}
241242
if isinstance(range_element_type, FieldElementType):
242243
self._properties["rangeElementType"] = range_element_type.to_api_repr()
243-
if isinstance(rounding_mode, RoundingMode):
244-
self._properties["roundingMode"] = rounding_mode.name
245-
if isinstance(rounding_mode, str):
246-
self._properties["roundingMode"] = rounding_mode
247-
if isinstance(foreign_type_definition, str):
248-
self._properties["foreignTypeDefinition"] = foreign_type_definition
249-
250-
# The order of operations is important:
251-
# If field_type is FOREIGN, then foreign_type_definition must be set.
252-
if field_type != "FOREIGN":
253-
self._properties["type"] = field_type
254-
else:
255-
if self._properties.get("foreignTypeDefinition") is None:
256-
raise ValueError(
257-
"If the 'field_type' is 'FOREIGN', then 'foreign_type_definition' is required."
258-
)
259-
self._properties["type"] = field_type
260-
261-
self._fields = tuple(fields)
244+
if fields: # Don't set the property if it's not set.
245+
self._properties["fields"] = [field.to_api_repr() for field in fields]
262246

263-
@staticmethod
264-
def __get_int(api_repr, name):
265-
v = api_repr.get(name, _DEFAULT_VALUE)
266-
if v is not _DEFAULT_VALUE:
267-
v = int(v)
268-
return v
269247

270248
@classmethod
271249
def from_api_repr(cls, api_repr: dict) -> "SchemaField":
@@ -279,48 +257,19 @@ def from_api_repr(cls, api_repr: dict) -> "SchemaField":
279257
Returns:
280258
google.cloud.bigquery.schema.SchemaField: The ``SchemaField`` object.
281259
"""
282-
field_type = api_repr["type"].upper()
260+
placeholder = cls("this_will_be_replaced", "PLACEHOLDER")
283261

284-
# Handle optional properties with default values
285-
mode = api_repr.get("mode", "NULLABLE")
286-
description = api_repr.get("description", _DEFAULT_VALUE)
287-
fields = api_repr.get("fields", ())
288-
policy_tags = api_repr.get("policyTags", _DEFAULT_VALUE)
262+
# Note: we don't make a copy of api_repr because this can cause
263+
# unnecessary slowdowns, especially on deeply nested STRUCT / RECORD
264+
# fields. See https://github.com/googleapis/python-bigquery/issues/6
265+
placeholder._properties = api_repr
289266

290-
default_value_expression = api_repr.get("defaultValueExpression", None)
291-
292-
if policy_tags is not None and policy_tags is not _DEFAULT_VALUE:
293-
policy_tags = PolicyTagList.from_api_repr(policy_tags)
294-
295-
if api_repr.get("rangeElementType"):
296-
range_element_type = cast(dict, api_repr.get("rangeElementType"))
297-
element_type = range_element_type.get("type")
298-
else:
299-
element_type = None
300-
301-
rounding_mode = api_repr.get("roundingMode")
302-
foreign_type_definition = api_repr.get("foreignTypeDefinition")
303-
304-
return cls(
305-
field_type=field_type,
306-
fields=[cls.from_api_repr(f) for f in fields],
307-
mode=mode.upper(),
308-
default_value_expression=default_value_expression,
309-
description=description,
310-
name=api_repr["name"],
311-
policy_tags=policy_tags,
312-
precision=cls.__get_int(api_repr, "precision"),
313-
scale=cls.__get_int(api_repr, "scale"),
314-
max_length=cls.__get_int(api_repr, "maxLength"),
315-
range_element_type=element_type,
316-
rounding_mode=rounding_mode,
317-
foreign_type_definition=foreign_type_definition,
318-
)
267+
return placeholder
319268

320269
@property
321270
def name(self):
322271
"""str: The name of the field."""
323-
return self._properties["name"]
272+
return self._properties.get("name", "")
324273

325274
@property
326275
def field_type(self):
@@ -329,7 +278,10 @@ def field_type(self):
329278
See:
330279
https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#TableFieldSchema.FIELDS.type
331280
"""
332-
return self._properties["type"]
281+
type_ = self._properties.get("type")
282+
if type_ is None: # Shouldn't happen, but some unit tests do this.
283+
return None
284+
return cast(str, type_).upper()
333285

334286
@property
335287
def mode(self):
@@ -338,7 +290,7 @@ def mode(self):
338290
See:
339291
https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#TableFieldSchema.FIELDS.mode
340292
"""
341-
return self._properties.get("mode")
293+
return cast(str, self._properties.get("mode", "NULLABLE")).upper()
342294

343295
@property
344296
def is_nullable(self):
@@ -358,17 +310,17 @@ def description(self):
358310
@property
359311
def precision(self):
360312
"""Optional[int]: Precision (number of digits) for the NUMERIC field."""
361-
return self._properties.get("precision")
313+
return _helpers._int_or_none(self._properties.get("precision"))
362314

363315
@property
364316
def scale(self):
365317
"""Optional[int]: Scale (digits after decimal) for the NUMERIC field."""
366-
return self._properties.get("scale")
318+
return _helpers._int_or_none(self._properties.get("scale"))
367319

368320
@property
369321
def max_length(self):
370322
"""Optional[int]: Maximum length for the STRING or BYTES field."""
371-
return self._properties.get("maxLength")
323+
return _helpers._int_or_none(self._properties.get("maxLength"))
372324

373325
@property
374326
def range_element_type(self):
@@ -404,7 +356,7 @@ def fields(self):
404356
405357
Must be empty unset if ``field_type`` is not 'RECORD'.
406358
"""
407-
return self._fields
359+
return tuple(_to_schema_fields(self._properties.get("fields", [])))
408360

409361
@property
410362
def policy_tags(self):
@@ -420,15 +372,10 @@ def to_api_repr(self) -> dict:
420372
Returns:
421373
Dict: A dictionary representing the SchemaField in a serialized form.
422374
"""
423-
answer = self._properties.copy()
424-
425-
# If this is a RECORD type, then sub-fields are also included,
426-
# add this to the serialized representation.
427-
if self.field_type.upper() in _STRUCT_TYPES:
428-
answer["fields"] = [f.to_api_repr() for f in self.fields]
429-
430-
# Done; return the serialized dictionary.
431-
return answer
375+
# Note: we don't make a copy of _properties because this can cause
376+
# unnecessary slowdowns, especially on deeply nested STRUCT / RECORD
377+
# fields. See https://github.com/googleapis/python-bigquery/issues/6
378+
return self._properties
432379

433380
def _key(self):
434381
"""A tuple key that uniquely describes this field.
@@ -464,7 +411,7 @@ def _key(self):
464411
self.mode.upper(), # pytype: disable=attribute-error
465412
self.default_value_expression,
466413
self.description,
467-
self._fields,
414+
self.fields,
468415
policy_tags,
469416
)
470417

tests/unit/job/test_load_config.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import copy
1516
import warnings
1617

1718
import pytest
@@ -571,16 +572,34 @@ def test_schema_setter_valid_mappings_list(self):
571572
config._properties["load"]["schema"], {"fields": [full_name_repr, age_repr]}
572573
)
573574

574-
def test_schema_setter_invalid_mappings_list(self):
575+
def test_schema_setter_allows_unknown_properties(self):
575576
config = self._get_target_class()()
576577

577578
schema = [
578-
{"name": "full_name", "type": "STRING", "mode": "REQUIRED"},
579-
{"name": "age", "typeoo": "INTEGER", "mode": "REQUIRED"},
579+
{
580+
"name": "full_name",
581+
"type": "STRING",
582+
"mode": "REQUIRED",
583+
"someNewProperty": "test-value",
584+
},
585+
{
586+
"name": "age",
587+
# Note: This type should be included, too. Avoid client-side
588+
# validation, as it could prevent backwards-compatible
589+
# evolution of the server-side behavior.
590+
"typo": "INTEGER",
591+
"mode": "REQUIRED",
592+
"anotherNewProperty": "another-test",
593+
},
580594
]
581595

582-
with self.assertRaises(Exception):
583-
config.schema = schema
596+
# Make sure the setter doesn't mutate schema.
597+
expected_schema = copy.deepcopy(schema)
598+
599+
config.schema = schema
600+
601+
# _properties should include all fields, including unknown ones.
602+
assert config._properties["load"]["schema"]["fields"] == expected_schema
584603

585604
def test_schema_setter_unsetting_schema(self):
586605
from google.cloud.bigquery.schema import SchemaField

tests/unit/test_schema.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
16+
import copy
17+
import unittest
18+
from unittest import mock
19+
20+
import pytest
21+
1522
from google.cloud import bigquery
1623
from google.cloud.bigquery.enums import RoundingMode
1724
from google.cloud.bigquery.standard_sql import StandardSqlStructType
@@ -22,11 +29,6 @@
2229
SerDeInfo,
2330
)
2431

25-
import unittest
26-
from unittest import mock
27-
28-
import pytest
29-
3032

3133
class TestSchemaField(unittest.TestCase):
3234
@staticmethod
@@ -877,13 +879,32 @@ def test_schema_fields_sequence(self):
877879
result = self._call_fut(schema)
878880
self.assertEqual(result, schema)
879881

880-
def test_invalid_mapping_representation(self):
882+
def test_unknown_properties(self):
881883
schema = [
882-
{"name": "full_name", "type": "STRING", "mode": "REQUIRED"},
883-
{"name": "address", "typeooo": "STRING", "mode": "REQUIRED"},
884+
{
885+
"name": "full_name",
886+
"type": "STRING",
887+
"mode": "REQUIRED",
888+
"someNewProperty": "test-value",
889+
},
890+
{
891+
"name": "age",
892+
# Note: This type should be included, too. Avoid client-side
893+
# validation, as it could prevent backwards-compatible
894+
# evolution of the server-side behavior.
895+
"typo": "INTEGER",
896+
"mode": "REQUIRED",
897+
"anotherNewProperty": "another-test",
898+
},
884899
]
885-
with self.assertRaises(Exception):
886-
self._call_fut(schema)
900+
901+
# Make sure the setter doesn't mutate schema.
902+
expected_schema = copy.deepcopy(schema)
903+
904+
result = self._call_fut(schema)
905+
906+
for api_repr, field in zip(expected_schema, result):
907+
assert field.to_api_repr() == api_repr
887908

888909
def test_valid_mapping_representation(self):
889910
from google.cloud.bigquery.schema import SchemaField

0 commit comments

Comments
 (0)