Skip to content

Commit

Permalink
Updates SchemaField with new fields and adds tests
Browse files Browse the repository at this point in the history
  • Loading branch information
chalmerlowe committed Nov 22, 2024
1 parent 236455c commit 808a925
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 4 deletions.
8 changes: 8 additions & 0 deletions google/cloud/bigquery/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ class SqlTypeNames(str, enum.Enum):
DATETIME = "DATETIME"
INTERVAL = "INTERVAL" # NOTE: not available in legacy types
RANGE = "RANGE" # NOTE: not available in legacy types
FOREIGN = "FOREIGN" # NOTE: type acts as a wrapper for data types
# not natively understood by BigQuery unless translated


class WriteDisposition(object):
Expand Down Expand Up @@ -344,3 +346,9 @@ class DeterminismLevel:

NOT_DETERMINISTIC = "NOT_DETERMINISTIC"
"""The UDF is not deterministic."""


class RoundingMode(enum.Enum):
ROUNDING_MODE_UNSPECIFIED = 0
ROUND_HALF_AWAY_FROM_ZERO = 1
ROUND_HALF_EVEN = 2
49 changes: 48 additions & 1 deletion google/cloud/bigquery/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
_from_api_repr,
_get_sub_prop,
)
from google.cloud.bigquery.enums import StandardSqlTypeNames
from google.cloud.bigquery.enums import StandardSqlTypeNames, RoundingMode


_STRUCT_TYPES = ("RECORD", "STRUCT")
Expand Down Expand Up @@ -186,6 +186,8 @@ def __init__(
scale: Union[int, _DefaultSentinel] = _DEFAULT_VALUE,
max_length: Union[int, _DefaultSentinel] = _DEFAULT_VALUE,
range_element_type: Union[FieldElementType, str, None] = None,
rounding_mode: Union[RoundingMode, str, None] = None,
foreign_type_definition: Optional[str] = None,
):
self._properties: Dict[str, Any] = {
"name": name,
Expand All @@ -211,6 +213,12 @@ def __init__(
self._properties["rangeElementType"] = {"type": range_element_type}
if isinstance(range_element_type, FieldElementType):
self._properties["rangeElementType"] = range_element_type.to_api_repr()
if isinstance(rounding_mode, RoundingMode):
self._properties["roundingMode"] = rounding_mode.name
if isinstance(rounding_mode, str):
self._properties["roundingMode"] = rounding_mode
if isinstance(foreign_type_definition, str):
self._properties["foreignTypeDefinition"] = foreign_type_definition

self._fields = tuple(fields)

Expand Down Expand Up @@ -252,6 +260,9 @@ def from_api_repr(cls, api_repr: dict) -> "SchemaField":
else:
element_type = None

rounding_mode = api_repr.get("roundingMode")
foreign_type_definition = api_repr.get("foreignTypeDefinition")

return cls(
field_type=field_type,
fields=[cls.from_api_repr(f) for f in fields],
Expand All @@ -264,6 +275,8 @@ def from_api_repr(cls, api_repr: dict) -> "SchemaField":
scale=cls.__get_int(api_repr, "scale"),
max_length=cls.__get_int(api_repr, "maxLength"),
range_element_type=element_type,
rounding_mode=rounding_mode,
foreign_type_definition=foreign_type_definition,
)

@property
Expand Down Expand Up @@ -331,6 +344,40 @@ def range_element_type(self):
ret = self._properties.get("rangeElementType")
return FieldElementType.from_api_repr(ret)

@property
def rounding_mode(self):
"""Specifies the rounding mode to be used when storing values of
NUMERIC and BIGNUMERIC type.
Unspecified will default to using ROUND_HALF_AWAY_FROM_ZERO.
ROUND_HALF_AWAY_FROM_ZERO rounds half values away from zero
when applying precision and scale upon writing of NUMERIC and BIGNUMERIC
values.
For Scale: 0
1.1, 1.2, 1.3, 1.4 => 1
1.5, 1.6, 1.7, 1.8, 1.9 => 2
ROUND_HALF_EVEN rounds half values to the nearest even value
when applying precision and scale upon writing of NUMERIC and BIGNUMERIC
values.
For Scale: 0
1.1, 1.2, 1.3, 1.4 => 1
1.5 => 2
1.6, 1.7, 1.8, 1.9 => 2
2.5 => 2
"""
return self._properties.get("roundingMode")

@property
def foreign_type_definition(self):
"""Definition of the foreign data type.
Only valid for top-level schema fields (not nested fields).
If the type is FOREIGN, this field is required.
"""
return self._properties.get("foreignTypeDefinition")

@property
def fields(self):
"""Optional[tuple]: Subfields contained in this field.
Expand Down
43 changes: 40 additions & 3 deletions tests/unit/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

from google.cloud import bigquery
from google.cloud.bigquery.enums import RoundingMode
from google.cloud.bigquery.standard_sql import StandardSqlStructType
from google.cloud.bigquery.schema import (
PolicyTagList,
Expand Down Expand Up @@ -52,9 +53,12 @@ def test_constructor_defaults(self):
self.assertEqual(field.fields, ())
self.assertIsNone(field.policy_tags)
self.assertIsNone(field.default_value_expression)
self.assertEqual(field.rounding_mode, None)
self.assertEqual(field.foreign_type_definition, None)

def test_constructor_explicit(self):
FIELD_DEFAULT_VALUE_EXPRESSION = "This is the default value for this field"
ROUNDINGMODE = RoundingMode.ROUNDING_MODE_UNSPECIFIED
field = self._make_one(
"test",
"STRING",
Expand All @@ -67,6 +71,8 @@ def test_constructor_explicit(self):
)
),
default_value_expression=FIELD_DEFAULT_VALUE_EXPRESSION,
rounding_mode=ROUNDINGMODE,
foreign_type_definition="INTEGER",
)
self.assertEqual(field.name, "test")
self.assertEqual(field.field_type, "STRING")
Expand All @@ -83,9 +89,16 @@ def test_constructor_explicit(self):
)
),
)
self.assertEqual(field.rounding_mode, ROUNDINGMODE.name)
self.assertEqual(field.foreign_type_definition, "INTEGER")

def test_constructor_explicit_none(self):
field = self._make_one("test", "STRING", description=None, policy_tags=None)
field = self._make_one(
"test",
"STRING",
description=None,
policy_tags=None,
)
self.assertIsNone(field.description)
self.assertIsNone(field.policy_tags)

Expand Down Expand Up @@ -141,10 +154,18 @@ def test_to_api_repr(self):
policy.to_api_repr(),
{"names": ["foo", "bar"]},
)
ROUNDINGMODE = RoundingMode.ROUNDING_MODE_UNSPECIFIED

field = self._make_one(
"foo", "INTEGER", "NULLABLE", description="hello world", policy_tags=policy
"foo",
"INTEGER",
"NULLABLE",
description="hello world",
policy_tags=policy,
rounding_mode=ROUNDINGMODE,
foreign_type_definition="INTEGER",
)

self.assertEqual(
field.to_api_repr(),
{
Expand All @@ -153,6 +174,8 @@ def test_to_api_repr(self):
"type": "INTEGER",
"description": "hello world",
"policyTags": {"names": ["foo", "bar"]},
"roundingMode": "ROUNDING_MODE_UNSPECIFIED",
"foreignTypeDefinition": "INTEGER",
},
)

Expand Down Expand Up @@ -186,6 +209,8 @@ def test_from_api_repr(self):
"description": "test_description",
"name": "foo",
"type": "record",
"roundingMode": "ROUNDING_MODE_UNSPECIFIED",
"foreignTypeDefinition": "INTEGER",
}
)
self.assertEqual(field.name, "foo")
Expand All @@ -197,6 +222,8 @@ def test_from_api_repr(self):
self.assertEqual(field.fields[0].field_type, "INTEGER")
self.assertEqual(field.fields[0].mode, "NULLABLE")
self.assertEqual(field.range_element_type, None)
self.assertEqual(field.rounding_mode, "ROUNDING_MODE_UNSPECIFIED")
self.assertEqual(field.foreign_type_definition, "INTEGER")

def test_from_api_repr_policy(self):
field = self._get_target_class().from_api_repr(
Expand Down Expand Up @@ -1117,7 +1144,17 @@ def test_to_api_repr_parameterized(field, api):


class TestForeignTypeInfo:
"""TODO: add doc string."""
"""Tests metadata re: the foreign data type definition in field schema.
Specifies the system which defines the foreign data type.
TypeSystems are external systems, such as query engines or table formats,
that have their own data types.
TypeSystem may be:
TypeSystem not specified: TYPE_SYSTEM_UNSPECIFIED
Represents Hive data types: HIVE
"""

@staticmethod
def _get_target_class():
Expand Down

0 comments on commit 808a925

Please sign in to comment.