Skip to content

Commit 3a48948

Browse files
authored
feat: add roundingmode enum, wiring, and tests (#2121)
* feat: adds roundingmode and entity types * Adds rounding_mode to schema file and tests * tweaks RoundingMode docstring and roundingmode logic * Updates tests to apply better coverage for rounding_mode * Modifies docstring * Removes client-side validation, simplifies some code * Updates foreign_type_definition processing
1 parent 3d62c16 commit 3a48948

File tree

3 files changed

+156
-4
lines changed

3 files changed

+156
-4
lines changed

google/cloud/bigquery/enums.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,11 @@ class KeyResultStatementKind:
246246

247247

248248
class StandardSqlTypeNames(str, enum.Enum):
249+
"""Enum of allowed SQL type names in schema.SchemaField.
250+
251+
Datatype used in GoogleSQL.
252+
"""
253+
249254
def _generate_next_value_(name, start, count, last_values):
250255
return name
251256

@@ -267,6 +272,9 @@ def _generate_next_value_(name, start, count, last_values):
267272
ARRAY = enum.auto()
268273
STRUCT = enum.auto()
269274
RANGE = enum.auto()
275+
# NOTE: FOREIGN acts as a wrapper for data types
276+
# not natively understood by BigQuery unless translated
277+
FOREIGN = enum.auto()
270278

271279

272280
class EntityTypes(str, enum.Enum):
@@ -285,7 +293,10 @@ class EntityTypes(str, enum.Enum):
285293
# See also: https://cloud.google.com/bigquery/data-types#legacy_sql_data_types
286294
# and https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types
287295
class SqlTypeNames(str, enum.Enum):
288-
"""Enum of allowed SQL type names in schema.SchemaField."""
296+
"""Enum of allowed SQL type names in schema.SchemaField.
297+
298+
Datatype used in Legacy SQL.
299+
"""
289300

290301
STRING = "STRING"
291302
BYTES = "BYTES"
@@ -306,6 +317,9 @@ class SqlTypeNames(str, enum.Enum):
306317
DATETIME = "DATETIME"
307318
INTERVAL = "INTERVAL" # NOTE: not available in legacy types
308319
RANGE = "RANGE" # NOTE: not available in legacy types
320+
# NOTE: FOREIGN acts as a wrapper for data types
321+
# not natively understood by BigQuery unless translated
322+
FOREIGN = "FOREIGN"
309323

310324

311325
class WriteDisposition(object):
@@ -344,3 +358,32 @@ class DeterminismLevel:
344358

345359
NOT_DETERMINISTIC = "NOT_DETERMINISTIC"
346360
"""The UDF is not deterministic."""
361+
362+
363+
class RoundingMode(str, enum.Enum):
364+
"""Rounding mode options that can be used when storing NUMERIC or BIGNUMERIC
365+
values.
366+
367+
ROUNDING_MODE_UNSPECIFIED: will default to using ROUND_HALF_AWAY_FROM_ZERO.
368+
369+
ROUND_HALF_AWAY_FROM_ZERO: rounds half values away from zero when applying
370+
precision and scale upon writing of NUMERIC and BIGNUMERIC values.
371+
For Scale: 0
372+
* 1.1, 1.2, 1.3, 1.4 => 1
373+
* 1.5, 1.6, 1.7, 1.8, 1.9 => 2
374+
375+
ROUND_HALF_EVEN: rounds half values to the nearest even value when applying
376+
precision and scale upon writing of NUMERIC and BIGNUMERIC values.
377+
For Scale: 0
378+
* 1.1, 1.2, 1.3, 1.4 => 1
379+
* 1.5 => 2
380+
* 1.6, 1.7, 1.8, 1.9 => 2
381+
* 2.5 => 2
382+
"""
383+
384+
def _generate_next_value_(name, start, count, last_values):
385+
return name
386+
387+
ROUNDING_MODE_UNSPECIFIED = enum.auto()
388+
ROUND_HALF_AWAY_FROM_ZERO = enum.auto()
389+
ROUND_HALF_EVEN = enum.auto()

google/cloud/bigquery/schema.py

+57-2
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@
2222

2323
from google.cloud.bigquery import _helpers
2424
from google.cloud.bigquery import standard_sql
25+
from google.cloud.bigquery import enums
2526
from google.cloud.bigquery.enums import StandardSqlTypeNames
2627

2728

2829
_STRUCT_TYPES = ("RECORD", "STRUCT")
2930

3031
# SQL types reference:
31-
# https://cloud.google.com/bigquery/data-types#legacy_sql_data_types
32-
# https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types
32+
# LEGACY SQL: https://cloud.google.com/bigquery/data-types#legacy_sql_data_types
33+
# GoogleSQL: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types
3334
LEGACY_TO_STANDARD_TYPES = {
3435
"STRING": StandardSqlTypeNames.STRING,
3536
"BYTES": StandardSqlTypeNames.BYTES,
@@ -48,6 +49,7 @@
4849
"DATE": StandardSqlTypeNames.DATE,
4950
"TIME": StandardSqlTypeNames.TIME,
5051
"DATETIME": StandardSqlTypeNames.DATETIME,
52+
"FOREIGN": StandardSqlTypeNames.FOREIGN,
5153
# no direct conversion from ARRAY, the latter is represented by mode="REPEATED"
5254
}
5355
"""String names of the legacy SQL types to integer codes of Standard SQL standard_sql."""
@@ -166,6 +168,35 @@ class SchemaField(object):
166168
the type is RANGE, this field is required. Possible values for the
167169
field element type of a RANGE include `DATE`, `DATETIME` and
168170
`TIMESTAMP`.
171+
172+
rounding_mode: Union[enums.RoundingMode, str, None]
173+
Specifies the rounding mode to be used when storing values of
174+
NUMERIC and BIGNUMERIC type.
175+
176+
Unspecified will default to using ROUND_HALF_AWAY_FROM_ZERO.
177+
ROUND_HALF_AWAY_FROM_ZERO rounds half values away from zero
178+
when applying precision and scale upon writing of NUMERIC and BIGNUMERIC
179+
values.
180+
181+
For Scale: 0
182+
1.1, 1.2, 1.3, 1.4 => 1
183+
1.5, 1.6, 1.7, 1.8, 1.9 => 2
184+
185+
ROUND_HALF_EVEN rounds half values to the nearest even value
186+
when applying precision and scale upon writing of NUMERIC and BIGNUMERIC
187+
values.
188+
189+
For Scale: 0
190+
1.1, 1.2, 1.3, 1.4 => 1
191+
1.5 => 2
192+
1.6, 1.7, 1.8, 1.9 => 2
193+
2.5 => 2
194+
195+
foreign_type_definition: Optional[str]
196+
Definition of the foreign data type.
197+
198+
Only valid for top-level schema fields (not nested fields).
199+
If the type is FOREIGN, this field is required.
169200
"""
170201

171202
def __init__(
@@ -181,11 +212,14 @@ def __init__(
181212
scale: Union[int, _DefaultSentinel] = _DEFAULT_VALUE,
182213
max_length: Union[int, _DefaultSentinel] = _DEFAULT_VALUE,
183214
range_element_type: Union[FieldElementType, str, None] = None,
215+
rounding_mode: Union[enums.RoundingMode, str, None] = None,
216+
foreign_type_definition: Optional[str] = None,
184217
):
185218
self._properties: Dict[str, Any] = {
186219
"name": name,
187220
"type": field_type,
188221
}
222+
self._properties["name"] = name
189223
if mode is not None:
190224
self._properties["mode"] = mode.upper()
191225
if description is not _DEFAULT_VALUE:
@@ -206,6 +240,11 @@ def __init__(
206240
self._properties["rangeElementType"] = {"type": range_element_type}
207241
if isinstance(range_element_type, FieldElementType):
208242
self._properties["rangeElementType"] = range_element_type.to_api_repr()
243+
if rounding_mode is not None:
244+
self._properties["roundingMode"] = rounding_mode
245+
if foreign_type_definition is not None:
246+
self._properties["foreignTypeDefinition"] = foreign_type_definition
247+
209248
if fields: # Don't set the property if it's not set.
210249
self._properties["fields"] = [field.to_api_repr() for field in fields]
211250

@@ -304,6 +343,22 @@ def range_element_type(self):
304343
ret = self._properties.get("rangeElementType")
305344
return FieldElementType.from_api_repr(ret)
306345

346+
@property
347+
def rounding_mode(self):
348+
"""Enum that specifies the rounding mode to be used when storing values of
349+
NUMERIC and BIGNUMERIC type.
350+
"""
351+
return self._properties.get("roundingMode")
352+
353+
@property
354+
def foreign_type_definition(self):
355+
"""Definition of the foreign data type.
356+
357+
Only valid for top-level schema fields (not nested fields).
358+
If the type is FOREIGN, this field is required.
359+
"""
360+
return self._properties.get("foreignTypeDefinition")
361+
307362
@property
308363
def fields(self):
309364
"""Optional[tuple]: Subfields contained in this field.

tests/unit/test_schema.py

+55-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import pytest
2020

2121
from google.cloud import bigquery
22+
from google.cloud.bigquery import enums
2223
from google.cloud.bigquery.standard_sql import StandardSqlStructType
2324
from google.cloud.bigquery import schema
2425
from google.cloud.bigquery.schema import PolicyTagList
@@ -49,6 +50,8 @@ def test_constructor_defaults(self):
4950
self.assertEqual(field.fields, ())
5051
self.assertIsNone(field.policy_tags)
5152
self.assertIsNone(field.default_value_expression)
53+
self.assertEqual(field.rounding_mode, None)
54+
self.assertEqual(field.foreign_type_definition, None)
5255

5356
def test_constructor_explicit(self):
5457
FIELD_DEFAULT_VALUE_EXPRESSION = "This is the default value for this field"
@@ -64,6 +67,8 @@ def test_constructor_explicit(self):
6467
)
6568
),
6669
default_value_expression=FIELD_DEFAULT_VALUE_EXPRESSION,
70+
rounding_mode=enums.RoundingMode.ROUNDING_MODE_UNSPECIFIED,
71+
foreign_type_definition="INTEGER",
6772
)
6873
self.assertEqual(field.name, "test")
6974
self.assertEqual(field.field_type, "STRING")
@@ -80,6 +85,8 @@ def test_constructor_explicit(self):
8085
)
8186
),
8287
)
88+
self.assertEqual(field.rounding_mode, "ROUNDING_MODE_UNSPECIFIED")
89+
self.assertEqual(field.foreign_type_definition, "INTEGER")
8390

8491
def test_constructor_explicit_none(self):
8592
field = self._make_one("test", "STRING", description=None, policy_tags=None)
@@ -137,8 +144,16 @@ def test_to_api_repr(self):
137144
{"names": ["foo", "bar"]},
138145
)
139146

147+
ROUNDINGMODE = enums.RoundingMode.ROUNDING_MODE_UNSPECIFIED
148+
140149
field = self._make_one(
141-
"foo", "INTEGER", "NULLABLE", description="hello world", policy_tags=policy
150+
"foo",
151+
"INTEGER",
152+
"NULLABLE",
153+
description="hello world",
154+
policy_tags=policy,
155+
rounding_mode=ROUNDINGMODE,
156+
foreign_type_definition=None,
142157
)
143158
self.assertEqual(
144159
field.to_api_repr(),
@@ -148,6 +163,7 @@ def test_to_api_repr(self):
148163
"type": "INTEGER",
149164
"description": "hello world",
150165
"policyTags": {"names": ["foo", "bar"]},
166+
"roundingMode": "ROUNDING_MODE_UNSPECIFIED",
151167
},
152168
)
153169

@@ -181,6 +197,7 @@ def test_from_api_repr(self):
181197
"description": "test_description",
182198
"name": "foo",
183199
"type": "record",
200+
"roundingMode": "ROUNDING_MODE_UNSPECIFIED",
184201
}
185202
)
186203
self.assertEqual(field.name, "foo")
@@ -192,6 +209,7 @@ def test_from_api_repr(self):
192209
self.assertEqual(field.fields[0].field_type, "INTEGER")
193210
self.assertEqual(field.fields[0].mode, "NULLABLE")
194211
self.assertEqual(field.range_element_type, None)
212+
self.assertEqual(field.rounding_mode, "ROUNDING_MODE_UNSPECIFIED")
195213

196214
def test_from_api_repr_policy(self):
197215
field = self._get_target_class().from_api_repr(
@@ -283,6 +301,28 @@ def test_fields_property(self):
283301
schema_field = self._make_one("boat", "RECORD", fields=fields)
284302
self.assertEqual(schema_field.fields, fields)
285303

304+
def test_roundingmode_property_str(self):
305+
ROUNDINGMODE = "ROUND_HALF_AWAY_FROM_ZERO"
306+
schema_field = self._make_one("test", "STRING", rounding_mode=ROUNDINGMODE)
307+
self.assertEqual(schema_field.rounding_mode, ROUNDINGMODE)
308+
309+
del schema_field
310+
schema_field = self._make_one("test", "STRING")
311+
schema_field._properties["roundingMode"] = ROUNDINGMODE
312+
self.assertEqual(schema_field.rounding_mode, ROUNDINGMODE)
313+
314+
def test_foreign_type_definition_property_str(self):
315+
FOREIGN_TYPE_DEFINITION = "INTEGER"
316+
schema_field = self._make_one(
317+
"test", "STRING", foreign_type_definition=FOREIGN_TYPE_DEFINITION
318+
)
319+
self.assertEqual(schema_field.foreign_type_definition, FOREIGN_TYPE_DEFINITION)
320+
321+
del schema_field
322+
schema_field = self._make_one("test", "STRING")
323+
schema_field._properties["foreignTypeDefinition"] = FOREIGN_TYPE_DEFINITION
324+
self.assertEqual(schema_field.foreign_type_definition, FOREIGN_TYPE_DEFINITION)
325+
286326
def test_to_standard_sql_simple_type(self):
287327
examples = (
288328
# a few legacy types
@@ -457,6 +497,20 @@ def test_to_standard_sql_unknown_type(self):
457497
bigquery.StandardSqlTypeNames.TYPE_KIND_UNSPECIFIED,
458498
)
459499

500+
def test_to_standard_sql_foreign_type_valid(self):
501+
legacy_type = "FOREIGN"
502+
standard_type = bigquery.StandardSqlTypeNames.FOREIGN
503+
foreign_type_definition = "INTEGER"
504+
505+
field = self._make_one(
506+
"some_field",
507+
field_type=legacy_type,
508+
foreign_type_definition=foreign_type_definition,
509+
)
510+
standard_field = field.to_standard_sql()
511+
self.assertEqual(standard_field.name, "some_field")
512+
self.assertEqual(standard_field.type.type_kind, standard_type)
513+
460514
def test___eq___wrong_type(self):
461515
field = self._make_one("test", "STRING")
462516
other = object()

0 commit comments

Comments
 (0)