Skip to content

Commit 808a925

Browse files
committed
Updates SchemaField with new fields and adds tests
1 parent 236455c commit 808a925

File tree

3 files changed

+96
-4
lines changed

3 files changed

+96
-4
lines changed

google/cloud/bigquery/enums.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ class SqlTypeNames(str, enum.Enum):
306306
DATETIME = "DATETIME"
307307
INTERVAL = "INTERVAL" # NOTE: not available in legacy types
308308
RANGE = "RANGE" # NOTE: not available in legacy types
309+
FOREIGN = "FOREIGN" # NOTE: type acts as a wrapper for data types
310+
# not natively understood by BigQuery unless translated
309311

310312

311313
class WriteDisposition(object):
@@ -344,3 +346,9 @@ class DeterminismLevel:
344346

345347
NOT_DETERMINISTIC = "NOT_DETERMINISTIC"
346348
"""The UDF is not deterministic."""
349+
350+
351+
class RoundingMode(enum.Enum):
352+
ROUNDING_MODE_UNSPECIFIED = 0
353+
ROUND_HALF_AWAY_FROM_ZERO = 1
354+
ROUND_HALF_EVEN = 2

google/cloud/bigquery/schema.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
_from_api_repr,
2828
_get_sub_prop,
2929
)
30-
from google.cloud.bigquery.enums import StandardSqlTypeNames
30+
from google.cloud.bigquery.enums import StandardSqlTypeNames, RoundingMode
3131

3232

3333
_STRUCT_TYPES = ("RECORD", "STRUCT")
@@ -186,6 +186,8 @@ def __init__(
186186
scale: Union[int, _DefaultSentinel] = _DEFAULT_VALUE,
187187
max_length: Union[int, _DefaultSentinel] = _DEFAULT_VALUE,
188188
range_element_type: Union[FieldElementType, str, None] = None,
189+
rounding_mode: Union[RoundingMode, str, None] = None,
190+
foreign_type_definition: Optional[str] = None,
189191
):
190192
self._properties: Dict[str, Any] = {
191193
"name": name,
@@ -211,6 +213,12 @@ def __init__(
211213
self._properties["rangeElementType"] = {"type": range_element_type}
212214
if isinstance(range_element_type, FieldElementType):
213215
self._properties["rangeElementType"] = range_element_type.to_api_repr()
216+
if isinstance(rounding_mode, RoundingMode):
217+
self._properties["roundingMode"] = rounding_mode.name
218+
if isinstance(rounding_mode, str):
219+
self._properties["roundingMode"] = rounding_mode
220+
if isinstance(foreign_type_definition, str):
221+
self._properties["foreignTypeDefinition"] = foreign_type_definition
214222

215223
self._fields = tuple(fields)
216224

@@ -252,6 +260,9 @@ def from_api_repr(cls, api_repr: dict) -> "SchemaField":
252260
else:
253261
element_type = None
254262

263+
rounding_mode = api_repr.get("roundingMode")
264+
foreign_type_definition = api_repr.get("foreignTypeDefinition")
265+
255266
return cls(
256267
field_type=field_type,
257268
fields=[cls.from_api_repr(f) for f in fields],
@@ -264,6 +275,8 @@ def from_api_repr(cls, api_repr: dict) -> "SchemaField":
264275
scale=cls.__get_int(api_repr, "scale"),
265276
max_length=cls.__get_int(api_repr, "maxLength"),
266277
range_element_type=element_type,
278+
rounding_mode=rounding_mode,
279+
foreign_type_definition=foreign_type_definition,
267280
)
268281

269282
@property
@@ -331,6 +344,40 @@ def range_element_type(self):
331344
ret = self._properties.get("rangeElementType")
332345
return FieldElementType.from_api_repr(ret)
333346

347+
@property
348+
def rounding_mode(self):
349+
"""Specifies the rounding mode to be used when storing values of
350+
NUMERIC and BIGNUMERIC type.
351+
352+
Unspecified will default to using ROUND_HALF_AWAY_FROM_ZERO.
353+
354+
ROUND_HALF_AWAY_FROM_ZERO rounds half values away from zero
355+
when applying precision and scale upon writing of NUMERIC and BIGNUMERIC
356+
values.
357+
For Scale: 0
358+
1.1, 1.2, 1.3, 1.4 => 1
359+
1.5, 1.6, 1.7, 1.8, 1.9 => 2
360+
361+
ROUND_HALF_EVEN rounds half values to the nearest even value
362+
when applying precision and scale upon writing of NUMERIC and BIGNUMERIC
363+
values.
364+
For Scale: 0
365+
1.1, 1.2, 1.3, 1.4 => 1
366+
1.5 => 2
367+
1.6, 1.7, 1.8, 1.9 => 2
368+
2.5 => 2
369+
"""
370+
return self._properties.get("roundingMode")
371+
372+
@property
373+
def foreign_type_definition(self):
374+
"""Definition of the foreign data type.
375+
376+
Only valid for top-level schema fields (not nested fields).
377+
If the type is FOREIGN, this field is required.
378+
"""
379+
return self._properties.get("foreignTypeDefinition")
380+
334381
@property
335382
def fields(self):
336383
"""Optional[tuple]: Subfields contained in this field.

tests/unit/test_schema.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
from google.cloud import bigquery
16+
from google.cloud.bigquery.enums import RoundingMode
1617
from google.cloud.bigquery.standard_sql import StandardSqlStructType
1718
from google.cloud.bigquery.schema import (
1819
PolicyTagList,
@@ -52,9 +53,12 @@ def test_constructor_defaults(self):
5253
self.assertEqual(field.fields, ())
5354
self.assertIsNone(field.policy_tags)
5455
self.assertIsNone(field.default_value_expression)
56+
self.assertEqual(field.rounding_mode, None)
57+
self.assertEqual(field.foreign_type_definition, None)
5558

5659
def test_constructor_explicit(self):
5760
FIELD_DEFAULT_VALUE_EXPRESSION = "This is the default value for this field"
61+
ROUNDINGMODE = RoundingMode.ROUNDING_MODE_UNSPECIFIED
5862
field = self._make_one(
5963
"test",
6064
"STRING",
@@ -67,6 +71,8 @@ def test_constructor_explicit(self):
6771
)
6872
),
6973
default_value_expression=FIELD_DEFAULT_VALUE_EXPRESSION,
74+
rounding_mode=ROUNDINGMODE,
75+
foreign_type_definition="INTEGER",
7076
)
7177
self.assertEqual(field.name, "test")
7278
self.assertEqual(field.field_type, "STRING")
@@ -83,9 +89,16 @@ def test_constructor_explicit(self):
8389
)
8490
),
8591
)
92+
self.assertEqual(field.rounding_mode, ROUNDINGMODE.name)
93+
self.assertEqual(field.foreign_type_definition, "INTEGER")
8694

8795
def test_constructor_explicit_none(self):
88-
field = self._make_one("test", "STRING", description=None, policy_tags=None)
96+
field = self._make_one(
97+
"test",
98+
"STRING",
99+
description=None,
100+
policy_tags=None,
101+
)
89102
self.assertIsNone(field.description)
90103
self.assertIsNone(field.policy_tags)
91104

@@ -141,10 +154,18 @@ def test_to_api_repr(self):
141154
policy.to_api_repr(),
142155
{"names": ["foo", "bar"]},
143156
)
157+
ROUNDINGMODE = RoundingMode.ROUNDING_MODE_UNSPECIFIED
144158

145159
field = self._make_one(
146-
"foo", "INTEGER", "NULLABLE", description="hello world", policy_tags=policy
160+
"foo",
161+
"INTEGER",
162+
"NULLABLE",
163+
description="hello world",
164+
policy_tags=policy,
165+
rounding_mode=ROUNDINGMODE,
166+
foreign_type_definition="INTEGER",
147167
)
168+
148169
self.assertEqual(
149170
field.to_api_repr(),
150171
{
@@ -153,6 +174,8 @@ def test_to_api_repr(self):
153174
"type": "INTEGER",
154175
"description": "hello world",
155176
"policyTags": {"names": ["foo", "bar"]},
177+
"roundingMode": "ROUNDING_MODE_UNSPECIFIED",
178+
"foreignTypeDefinition": "INTEGER",
156179
},
157180
)
158181

@@ -186,6 +209,8 @@ def test_from_api_repr(self):
186209
"description": "test_description",
187210
"name": "foo",
188211
"type": "record",
212+
"roundingMode": "ROUNDING_MODE_UNSPECIFIED",
213+
"foreignTypeDefinition": "INTEGER",
189214
}
190215
)
191216
self.assertEqual(field.name, "foo")
@@ -197,6 +222,8 @@ def test_from_api_repr(self):
197222
self.assertEqual(field.fields[0].field_type, "INTEGER")
198223
self.assertEqual(field.fields[0].mode, "NULLABLE")
199224
self.assertEqual(field.range_element_type, None)
225+
self.assertEqual(field.rounding_mode, "ROUNDING_MODE_UNSPECIFIED")
226+
self.assertEqual(field.foreign_type_definition, "INTEGER")
200227

201228
def test_from_api_repr_policy(self):
202229
field = self._get_target_class().from_api_repr(
@@ -1117,7 +1144,17 @@ def test_to_api_repr_parameterized(field, api):
11171144

11181145

11191146
class TestForeignTypeInfo:
1120-
"""TODO: add doc string."""
1147+
"""Tests metadata re: the foreign data type definition in field schema.
1148+
1149+
Specifies the system which defines the foreign data type.
1150+
1151+
TypeSystems are external systems, such as query engines or table formats,
1152+
that have their own data types.
1153+
1154+
TypeSystem may be:
1155+
TypeSystem not specified: TYPE_SYSTEM_UNSPECIFIED
1156+
Represents Hive data types: HIVE
1157+
"""
11211158

11221159
@staticmethod
11231160
def _get_target_class():

0 commit comments

Comments
 (0)