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

Lines changed: 44 additions & 1 deletion
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

Lines changed: 57 additions & 2 deletions
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

Lines changed: 55 additions & 1 deletion
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy