From b359a9a55936a759a36aa69c5e5b014685e1fca6 Mon Sep 17 00:00:00 2001 From: Lingqing Gan Date: Mon, 4 Mar 2024 11:06:07 -0800 Subject: [PATCH] feat: support RANGE query parameters (#1827) * feat: RANGE query parameters and unit tests * unit test * unit test coverage * lint * lint * lint * system test * fix system test * ajust init items order * fix typos and improve docstrings --- benchmark/benchmark.py | 2 +- google/cloud/bigquery/__init__.py | 4 + google/cloud/bigquery/query.py | 297 ++++++++++++++++ tests/system/test_query.py | 33 ++ tests/unit/test_query.py | 548 ++++++++++++++++++++++++++++++ 5 files changed, 883 insertions(+), 1 deletion(-) diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index 30e294baa..d7dc78678 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -231,7 +231,7 @@ def _is_datetime_min(time_str: str) -> bool: def _summary(run: dict) -> str: - """Coverts run dict to run summary string.""" + """Converts run dict to run summary string.""" no_val = "NODATA" output = ["QUERYTIME "] diff --git a/google/cloud/bigquery/__init__.py b/google/cloud/bigquery/__init__.py index 1ea056eb8..caf81d9aa 100644 --- a/google/cloud/bigquery/__init__.py +++ b/google/cloud/bigquery/__init__.py @@ -83,6 +83,8 @@ from google.cloud.bigquery.query import ConnectionProperty from google.cloud.bigquery.query import ScalarQueryParameter from google.cloud.bigquery.query import ScalarQueryParameterType +from google.cloud.bigquery.query import RangeQueryParameter +from google.cloud.bigquery.query import RangeQueryParameterType from google.cloud.bigquery.query import SqlParameterScalarTypes from google.cloud.bigquery.query import StructQueryParameter from google.cloud.bigquery.query import StructQueryParameterType @@ -122,10 +124,12 @@ "ArrayQueryParameter", "ScalarQueryParameter", "StructQueryParameter", + "RangeQueryParameter", "ArrayQueryParameterType", "ScalarQueryParameterType", "SqlParameterScalarTypes", "StructQueryParameterType", + "RangeQueryParameterType", # Datasets "Dataset", "DatasetReference", diff --git a/google/cloud/bigquery/query.py b/google/cloud/bigquery/query.py index a06ece503..9c9402b74 100644 --- a/google/cloud/bigquery/query.py +++ b/google/cloud/bigquery/query.py @@ -30,6 +30,8 @@ Union[str, int, float, decimal.Decimal, bool, datetime.datetime, datetime.date] ] +_RANGE_ELEMENT_TYPE_STR = {"TIMESTAMP", "DATETIME", "DATE"} + class ConnectionProperty: """A connection-level property to customize query behavior. @@ -362,6 +364,129 @@ def __repr__(self): return f"{self.__class__.__name__}({items}{name}{description})" +class RangeQueryParameterType(_AbstractQueryParameterType): + """Type representation for range query parameters. + + Args: + type_ (Union[ScalarQueryParameterType, str]): + Type of range element, must be one of 'TIMESTAMP', 'DATETIME', or + 'DATE'. + name (Optional[str]): + The name of the query parameter. Primarily used if the type is + one of the subfields in ``StructQueryParameterType`` instance. + description (Optional[str]): + The query parameter description. Primarily used if the type is + one of the subfields in ``StructQueryParameterType`` instance. + """ + + @classmethod + def _parse_range_element_type(self, type_): + """Helper method that parses the input range element type, which may + be a string, or a ScalarQueryParameterType object. + + Returns: + google.cloud.bigquery.query.ScalarQueryParameterType: Instance + """ + if isinstance(type_, str): + if type_ not in _RANGE_ELEMENT_TYPE_STR: + raise ValueError( + "If given as a string, range element type must be one of " + "'TIMESTAMP', 'DATE', or 'DATETIME'." + ) + return ScalarQueryParameterType(type_) + elif isinstance(type_, ScalarQueryParameterType): + if type_._type not in _RANGE_ELEMENT_TYPE_STR: + raise ValueError( + "If given as a ScalarQueryParameter object, range element " + "type must be one of 'TIMESTAMP', 'DATE', or 'DATETIME' " + "type." + ) + return type_ + else: + raise ValueError( + "range_type must be a string or ScalarQueryParameter object, " + "of 'TIMESTAMP', 'DATE', or 'DATETIME' type." + ) + + def __init__(self, type_, *, name=None, description=None): + self.type_ = self._parse_range_element_type(type_) + self.name = name + self.description = description + + @classmethod + def from_api_repr(cls, resource): + """Factory: construct parameter type from JSON resource. + + Args: + resource (Dict): JSON mapping of parameter + + Returns: + google.cloud.bigquery.query.RangeQueryParameterType: Instance + """ + type_ = resource["rangeElementType"]["type"] + name = resource.get("name") + description = resource.get("description") + + return cls(type_, name=name, description=description) + + def to_api_repr(self): + """Construct JSON API representation for the parameter type. + + Returns: + Dict: JSON mapping + """ + # Name and description are only used if the type is a field inside a struct + # type, but it's StructQueryParameterType's responsibilty to use these two + # attributes in the API representation when needed. Here we omit them. + return { + "type": "RANGE", + "rangeElementType": self.type_.to_api_repr(), + } + + def with_name(self, new_name: Union[str, None]): + """Return a copy of the instance with ``name`` set to ``new_name``. + + Args: + name (Union[str, None]): + The new name of the range query parameter type. If ``None``, + the existing name is cleared. + + Returns: + google.cloud.bigquery.query.RangeQueryParameterType: + A new instance with updated name. + """ + return type(self)(self.type_, name=new_name, description=self.description) + + def __repr__(self): + name = f", name={self.name!r}" if self.name is not None else "" + description = ( + f", description={self.description!r}" + if self.description is not None + else "" + ) + return f"{self.__class__.__name__}({self.type_!r}{name}{description})" + + def _key(self): + """A tuple key that uniquely describes this field. + + Used to compute this instance's hashcode and evaluate equality. + + Returns: + Tuple: The contents of this + :class:`~google.cloud.bigquery.query.RangeQueryParameterType`. + """ + type_ = self.type_.to_api_repr() + return (self.name, type_, self.description) + + def __eq__(self, other): + if not isinstance(other, RangeQueryParameterType): + return NotImplemented + return self._key() == other._key() + + def __ne__(self, other): + return not self == other + + class _AbstractQueryParameter(object): """Base class for named / positional query parameters.""" @@ -811,6 +936,178 @@ def __repr__(self): return "StructQueryParameter{}".format(self._key()) +class RangeQueryParameter(_AbstractQueryParameter): + """Named / positional query parameters for range values. + + Args: + range_element_type (Union[str, RangeQueryParameterType]): + The type of range elements. It must be one of 'TIMESTAMP', + 'DATE', or 'DATETIME'. + + start (Optional[Union[ScalarQueryParameter, str]]): + The start of the range value. Must be the same type as + range_element_type. If not provided, it's interpreted as UNBOUNDED. + + end (Optional[Union[ScalarQueryParameter, str]]): + The end of the range value. Must be the same type as + range_element_type. If not provided, it's interpreted as UNBOUNDED. + + name (Optional[str]): + Parameter name, used via ``@foo`` syntax. If None, the + parameter can only be addressed via position (``?``). + """ + + @classmethod + def _parse_range_element_type(self, range_element_type): + if isinstance(range_element_type, str): + if range_element_type not in _RANGE_ELEMENT_TYPE_STR: + raise ValueError( + "If given as a string, range_element_type must be one of " + f"'TIMESTAMP', 'DATE', or 'DATETIME'. Got {range_element_type}." + ) + return RangeQueryParameterType(range_element_type) + elif isinstance(range_element_type, RangeQueryParameterType): + if range_element_type.type_._type not in _RANGE_ELEMENT_TYPE_STR: + raise ValueError( + "If given as a RangeQueryParameterType object, " + "range_element_type must be one of 'TIMESTAMP', 'DATE', " + "or 'DATETIME' type." + ) + return range_element_type + else: + raise ValueError( + "range_element_type must be a string or " + "RangeQueryParameterType object, of 'TIMESTAMP', 'DATE', " + "or 'DATETIME' type. Got " + f"{type(range_element_type)}:{range_element_type}" + ) + + @classmethod + def _serialize_range_element_value(self, value, type_): + if value is None or isinstance(value, str): + return value + else: + converter = _SCALAR_VALUE_TO_JSON_PARAM.get(type_) + if converter is not None: + return converter(value) # type: ignore + else: + raise ValueError( + f"Cannot convert range element value from type {type_}, " + "must be one of the strings 'TIMESTAMP', 'DATE' " + "'DATETIME' or a RangeQueryParameterType object." + ) + + def __init__( + self, + range_element_type, + start=None, + end=None, + name=None, + ): + self.name = name + self.range_element_type = self._parse_range_element_type(range_element_type) + print(self.range_element_type.type_._type) + self.start = start + self.end = end + + @classmethod + def positional( + cls, range_element_type, start=None, end=None + ) -> "RangeQueryParameter": + """Factory for positional parameters. + + Args: + range_element_type (Union[str, RangeQueryParameterType]): + The type of range elements. It must be one of `'TIMESTAMP'`, + `'DATE'`, or `'DATETIME'`. + + start (Optional[Union[ScalarQueryParameter, str]]): + The start of the range value. Must be the same type as + range_element_type. If not provided, it's interpreted as + UNBOUNDED. + + end (Optional[Union[ScalarQueryParameter, str]]): + The end of the range value. Must be the same type as + range_element_type. If not provided, it's interpreted as + UNBOUNDED. + + Returns: + google.cloud.bigquery.query.RangeQueryParameter: Instance without + name. + """ + return cls(range_element_type, start, end) + + @classmethod + def from_api_repr(cls, resource: dict) -> "RangeQueryParameter": + """Factory: construct parameter from JSON resource. + + Args: + resource (Dict): JSON mapping of parameter + + Returns: + google.cloud.bigquery.query.RangeQueryParameter: Instance + """ + name = resource.get("name") + range_element_type = ( + resource.get("parameterType", {}).get("rangeElementType", {}).get("type") + ) + range_value = resource.get("parameterValue", {}).get("rangeValue", {}) + start = range_value.get("start", {}).get("value") + end = range_value.get("end", {}).get("value") + + return cls(range_element_type, start=start, end=end, name=name) + + def to_api_repr(self) -> dict: + """Construct JSON API representation for the parameter. + + Returns: + Dict: JSON mapping + """ + range_element_type = self.range_element_type.to_api_repr() + type_ = self.range_element_type.type_._type + start = self._serialize_range_element_value(self.start, type_) + end = self._serialize_range_element_value(self.end, type_) + resource = { + "parameterType": range_element_type, + "parameterValue": { + "rangeValue": { + "start": {"value": start}, + "end": {"value": end}, + }, + }, + } + + # distinguish between name not provided vs. name being empty string + if self.name is not None: + resource["name"] = self.name + + return resource + + def _key(self): + """A tuple key that uniquely describes this field. + + Used to compute this instance's hashcode and evaluate equality. + + Returns: + Tuple: The contents of this + :class:`~google.cloud.bigquery.query.RangeQueryParameter`. + """ + + range_element_type = self.range_element_type.to_api_repr() + return (self.name, range_element_type, self.start, self.end) + + def __eq__(self, other): + if not isinstance(other, RangeQueryParameter): + return NotImplemented + return self._key() == other._key() + + def __ne__(self, other): + return not self == other + + def __repr__(self): + return "RangeQueryParameter{}".format(self._key()) + + class SqlParameterScalarTypes: """Supported scalar SQL query parameter types as type objects.""" diff --git a/tests/system/test_query.py b/tests/system/test_query.py index 82be40693..0494272d9 100644 --- a/tests/system/test_query.py +++ b/tests/system/test_query.py @@ -26,6 +26,7 @@ from google.cloud.bigquery.query import ScalarQueryParameterType from google.cloud.bigquery.query import StructQueryParameter from google.cloud.bigquery.query import StructQueryParameterType +from google.cloud.bigquery.query import RangeQueryParameter @pytest.fixture(params=["INSERT", "QUERY"]) @@ -422,6 +423,38 @@ def test_query_statistics(bigquery_client, query_api_method): ) ], ), + ( + "SELECT @range_date", + "[2016-12-05, UNBOUNDED)", + [ + RangeQueryParameter( + name="range_date", + range_element_type="DATE", + start=datetime.date(2016, 12, 5), + ) + ], + ), + ( + "SELECT @range_datetime", + "[2016-12-05T00:00:00, UNBOUNDED)", + [ + RangeQueryParameter( + name="range_datetime", + range_element_type="DATETIME", + start=datetime.datetime(2016, 12, 5), + ) + ], + ), + ( + "SELECT @range_unbounded", + "[UNBOUNDED, UNBOUNDED)", + [ + RangeQueryParameter( + name="range_unbounded", + range_element_type="DATETIME", + ) + ], + ), ), ) def test_query_parameters( diff --git a/tests/unit/test_query.py b/tests/unit/test_query.py index 1704abac7..f511bf28d 100644 --- a/tests/unit/test_query.py +++ b/tests/unit/test_query.py @@ -376,6 +376,100 @@ def test_repr_all_optional_attrs(self): self.assertEqual(repr(param_type), expected) +class Test_RangeQueryParameterType(unittest.TestCase): + @staticmethod + def _get_target_class(): + from google.cloud.bigquery.query import RangeQueryParameterType + + return RangeQueryParameterType + + def _make_one(self, *args, **kw): + return self._get_target_class()(*args, **kw) + + def test_ctor_str(self): + param_type = self._make_one("DATE", name="foo", description="bar") + self.assertEqual(param_type.type_._type, "DATE") + self.assertEqual(param_type.name, "foo") + self.assertEqual(param_type.description, "bar") + + def test_ctor_type(self): + from google.cloud.bigquery import ScalarQueryParameterType + + scalar_type = ScalarQueryParameterType("DATE") + param_type = self._make_one(scalar_type, name="foo", description="bar") + self.assertEqual(param_type.type_._type, "DATE") + self.assertEqual(param_type.name, "foo") + self.assertEqual(param_type.description, "bar") + + def test_ctor_unsupported_type_str(self): + with self.assertRaises(ValueError): + self._make_one("TIME") + + def test_ctor_unsupported_type_type(self): + from google.cloud.bigquery import ScalarQueryParameterType + + scalar_type = ScalarQueryParameterType("TIME") + with self.assertRaises(ValueError): + self._make_one(scalar_type) + + def test_ctor_wrong_type(self): + with self.assertRaises(ValueError): + self._make_one(None) + + def test_from_api_repr(self): + RESOURCE = { + "type": "RANGE", + "rangeElementType": {"type": "DATE"}, + } + + klass = self._get_target_class() + result = klass.from_api_repr(RESOURCE) + self.assertEqual(result.type_._type, "DATE") + self.assertIsNone(result.name) + self.assertIsNone(result.description) + + def test_to_api_repr(self): + EXPECTED = { + "type": "RANGE", + "rangeElementType": {"type": "DATE"}, + } + param_type = self._make_one("DATE", name="foo", description="bar") + result = param_type.to_api_repr() + self.assertEqual(result, EXPECTED) + + def test__repr__(self): + param_type = self._make_one("DATE", name="foo", description="bar") + param_repr = "RangeQueryParameterType(ScalarQueryParameterType('DATE'), name='foo', description='bar')" + self.assertEqual(repr(param_type), param_repr) + + def test__eq__(self): + param_type1 = self._make_one("DATE", name="foo", description="bar") + self.assertEqual(param_type1, param_type1) + self.assertNotEqual(param_type1, object()) + + alias = self._make_one("DATE", name="foo", description="bar") + self.assertIsNot(param_type1, alias) + self.assertEqual(param_type1, alias) + + wrong_type = self._make_one("DATETIME", name="foo", description="bar") + self.assertNotEqual(param_type1, wrong_type) + + wrong_name = self._make_one("DATETIME", name="foo2", description="bar") + self.assertNotEqual(param_type1, wrong_name) + + wrong_description = self._make_one("DATETIME", name="foo", description="bar2") + self.assertNotEqual(param_type1, wrong_description) + + def test_with_name(self): + param_type1 = self._make_one("DATE", name="foo", description="bar") + param_type2 = param_type1.with_name("foo2") + + self.assertIsNot(param_type1, param_type2) + self.assertEqual(param_type2.type_._type, "DATE") + self.assertEqual(param_type2.name, "foo2") + self.assertEqual(param_type2.description, "bar") + + class Test__AbstractQueryParameter(unittest.TestCase): @staticmethod def _get_target_class(): @@ -663,6 +757,460 @@ def test___repr__(self): self.assertEqual(repr(field1), expected) +class Test_RangeQueryParameter(unittest.TestCase): + @staticmethod + def _get_target_class(): + from google.cloud.bigquery.query import RangeQueryParameter + + return RangeQueryParameter + + def _make_one(self, *args, **kw): + return self._get_target_class()(*args, **kw) + + def test_ctor(self): + from google.cloud.bigquery.query import RangeQueryParameterType + + range_element_type = RangeQueryParameterType(type_="DATE") + param = self._make_one( + range_element_type="DATE", start="2016-08-11", name="foo" + ) + self.assertEqual(param.name, "foo") + self.assertEqual(param.range_element_type, range_element_type) + self.assertEqual(param.start, "2016-08-11") + self.assertIs(param.end, None) + + def test_ctor_w_datetime_query_parameter_type_str(self): + from google.cloud.bigquery.query import RangeQueryParameterType + + range_element_type = RangeQueryParameterType(type_="DATETIME") + start_datetime = datetime.datetime(year=2020, month=12, day=31, hour=12) + end_datetime = datetime.datetime(year=2021, month=12, day=31, hour=12) + param = self._make_one( + range_element_type="DATETIME", + start=start_datetime, + end=end_datetime, + name="foo", + ) + self.assertEqual(param.range_element_type, range_element_type) + self.assertEqual(param.start, start_datetime) + self.assertEqual(param.end, end_datetime) + self.assertEqual(param.name, "foo") + + def test_ctor_w_datetime_query_parameter_type_type(self): + from google.cloud.bigquery.query import RangeQueryParameterType + + range_element_type = RangeQueryParameterType(type_="DATETIME") + param = self._make_one(range_element_type=range_element_type) + self.assertEqual(param.range_element_type, range_element_type) + self.assertEqual(param.start, None) + self.assertEqual(param.end, None) + self.assertEqual(param.name, None) + + def test_ctor_w_timestamp_query_parameter_typ_str(self): + from google.cloud.bigquery.query import RangeQueryParameterType + + range_element_type = RangeQueryParameterType(type_="TIMESTAMP") + start_datetime = datetime.datetime(year=2020, month=12, day=31, hour=12) + end_datetime = datetime.datetime(year=2021, month=12, day=31, hour=12) + param = self._make_one( + range_element_type="TIMESTAMP", + start=start_datetime, + end=end_datetime, + name="foo", + ) + self.assertEqual(param.range_element_type, range_element_type) + self.assertEqual(param.start, start_datetime) + self.assertEqual(param.end, end_datetime) + self.assertEqual(param.name, "foo") + + def test_ctor_w_timestamp_query_parameter_type_type(self): + from google.cloud.bigquery.query import RangeQueryParameterType + + range_element_type = RangeQueryParameterType(type_="TIMESTAMP") + param = self._make_one(range_element_type=range_element_type) + self.assertEqual(param.range_element_type, range_element_type) + self.assertEqual(param.start, None) + self.assertEqual(param.end, None) + self.assertEqual(param.name, None) + + def test_ctor_w_date_query_parameter_type_str(self): + from google.cloud.bigquery.query import RangeQueryParameterType + + range_element_type = RangeQueryParameterType(type_="DATE") + start_date = datetime.date(year=2020, month=12, day=31) + end_date = datetime.date(year=2021, month=12, day=31) + param = self._make_one( + range_element_type="DATE", + start=start_date, + end=end_date, + name="foo", + ) + self.assertEqual(param.range_element_type, range_element_type) + self.assertEqual(param.start, start_date) + self.assertEqual(param.end, end_date) + self.assertEqual(param.name, "foo") + + def test_ctor_w_date_query_parameter_type_type(self): + from google.cloud.bigquery.query import RangeQueryParameterType + + range_element_type = RangeQueryParameterType(type_="DATE") + param = self._make_one(range_element_type=range_element_type) + self.assertEqual(param.range_element_type, range_element_type) + self.assertEqual(param.start, None) + self.assertEqual(param.end, None) + self.assertEqual(param.name, None) + + def test_ctor_w_name_empty_str(self): + from google.cloud.bigquery.query import RangeQueryParameterType + + range_element_type = RangeQueryParameterType(type_="DATE") + param = self._make_one( + range_element_type="DATE", + name="", + ) + self.assertEqual(param.range_element_type, range_element_type) + self.assertIs(param.start, None) + self.assertIs(param.end, None) + self.assertEqual(param.name, "") + + def test_ctor_wo_value(self): + from google.cloud.bigquery.query import RangeQueryParameterType + + range_element_type = RangeQueryParameterType(type_="DATETIME") + param = self._make_one(range_element_type="DATETIME", name="foo") + self.assertEqual(param.range_element_type, range_element_type) + self.assertIs(param.start, None) + self.assertIs(param.end, None) + self.assertEqual(param.name, "foo") + + def test_ctor_w_unsupported_query_parameter_type_str(self): + with self.assertRaises(ValueError): + self._make_one(range_element_type="TIME", name="foo") + + def test_ctor_w_unsupported_query_parameter_type_type(self): + from google.cloud.bigquery.query import RangeQueryParameterType + + range_element_type = RangeQueryParameterType(type_="DATE") + range_element_type.type_._type = "TIME" + with self.assertRaises(ValueError): + self._make_one(range_element_type=range_element_type, name="foo") + + def test_ctor_w_unsupported_query_parameter_type_input(self): + with self.assertRaises(ValueError): + self._make_one(range_element_type=None, name="foo") + + def test_positional(self): + from google.cloud.bigquery.query import RangeQueryParameterType + + range_element_type = RangeQueryParameterType(type_="DATE") + klass = self._get_target_class() + param = klass.positional( + range_element_type="DATE", start="2016-08-11", end="2016-08-12" + ) + self.assertIs(param.name, None) + self.assertEqual(param.range_element_type, range_element_type) + self.assertEqual(param.start, "2016-08-11") + self.assertEqual(param.end, "2016-08-12") + + def test_from_api_repr_w_name(self): + from google.cloud.bigquery.query import RangeQueryParameterType + + RESOURCE = { + "name": "foo", + "parameterType": { + "type": "RANGE", + "rangeElementType": { + "type": "DATE", + }, + }, + "parameterValue": { + "rangeValue": {"start": {"value": None}, "end": {"value": "2020-12-31"}} + }, + } + klass = self._get_target_class() + param = klass.from_api_repr(RESOURCE) + range_element_type = RangeQueryParameterType(type_="DATE") + self.assertEqual(param.name, "foo") + self.assertEqual(param.range_element_type, range_element_type) + self.assertEqual(param.start, None) + self.assertEqual(param.end, "2020-12-31") + + def test_from_api_repr_wo_name(self): + from google.cloud.bigquery.query import RangeQueryParameterType + + RESOURCE = { + "parameterType": { + "type": "RANGE", + "rangeElementType": { + "type": "DATE", + }, + }, + "parameterValue": { + "rangeValue": {"start": {"value": None}, "end": {"value": "2020-12-31"}} + }, + } + klass = self._get_target_class() + param = klass.from_api_repr(RESOURCE) + range_element_type = RangeQueryParameterType(type_="DATE") + self.assertEqual(param.name, None) + self.assertEqual(param.range_element_type, range_element_type) + self.assertEqual(param.start, None) + self.assertEqual(param.end, "2020-12-31") + + def test_from_api_repr_wo_value(self): + # Back-end may not send back values for None params. See #9027 + from google.cloud.bigquery.query import RangeQueryParameterType + + RESOURCE = { + "parameterType": { + "type": "RANGE", + "rangeElementType": { + "type": "DATE", + }, + }, + } + range_element_type = RangeQueryParameterType(type_="DATE") + klass = self._get_target_class() + param = klass.from_api_repr(RESOURCE) + self.assertIs(param.name, None) + self.assertEqual(param.range_element_type, range_element_type) + self.assertIs(param.start, None) + self.assertIs(param.end, None) + + def test_to_api_repr_w_name(self): + EXPECTED = { + "name": "foo", + "parameterType": { + "type": "RANGE", + "rangeElementType": { + "type": "DATE", + }, + }, + "parameterValue": { + "rangeValue": {"start": {"value": None}, "end": {"value": "2016-08-11"}} + }, + } + param = self._make_one(range_element_type="DATE", end="2016-08-11", name="foo") + self.assertEqual(param.to_api_repr(), EXPECTED) + + def test_to_api_repr_wo_name(self): + EXPECTED = { + "parameterType": { + "type": "RANGE", + "rangeElementType": { + "type": "DATE", + }, + }, + "parameterValue": { + "rangeValue": {"start": {"value": None}, "end": {"value": "2016-08-11"}} + }, + } + klass = self._get_target_class() + param = klass.positional(range_element_type="DATE", end="2016-08-11") + self.assertEqual(param.to_api_repr(), EXPECTED) + + def test_to_api_repr_w_date_date(self): + today = datetime.date.today() + today_str = today.strftime("%Y-%m-%d") + EXPECTED = { + "name": "foo", + "parameterType": { + "type": "RANGE", + "rangeElementType": { + "type": "DATE", + }, + }, + "parameterValue": { + "rangeValue": {"start": {"value": None}, "end": {"value": today_str}} + }, + } + param = self._make_one(range_element_type="DATE", end=today, name="foo") + self.assertEqual(param.to_api_repr(), EXPECTED) + + def test_to_api_repr_w_datetime_str(self): + EXPECTED = { + "parameterType": { + "type": "RANGE", + "rangeElementType": { + "type": "DATETIME", + }, + }, + "parameterValue": { + "rangeValue": { + "start": {"value": None}, + "end": {"value": "2020-01-01T12:00:00.000000"}, + } + }, + } + klass = self._get_target_class() + end_datetime = datetime.datetime(year=2020, month=1, day=1, hour=12) + param = klass.positional(range_element_type="DATETIME", end=end_datetime) + self.assertEqual(param.to_api_repr(), EXPECTED) + + def test_to_api_repr_w_datetime_datetime(self): + from google.cloud.bigquery._helpers import _RFC3339_MICROS_NO_ZULU + + now = datetime.datetime.utcnow() + now_str = now.strftime(_RFC3339_MICROS_NO_ZULU) + EXPECTED = { + "parameterType": { + "type": "RANGE", + "rangeElementType": { + "type": "DATETIME", + }, + }, + "parameterValue": { + "rangeValue": {"start": {"value": None}, "end": {"value": now_str}} + }, + } + klass = self._get_target_class() + param = klass.positional(range_element_type="DATETIME", end=now) + self.assertEqual(param.to_api_repr(), EXPECTED) + + def test_to_api_repr_w_timestamp_str(self): + EXPECTED = { + "parameterType": { + "type": "RANGE", + "rangeElementType": { + "type": "TIMESTAMP", + }, + }, + "parameterValue": { + "rangeValue": { + "start": {"value": None}, + "end": {"value": "2020-01-01 12:00:00+00:00"}, + } + }, + } + klass = self._get_target_class() + end_timestamp = datetime.datetime(year=2020, month=1, day=1, hour=12) + param = klass.positional(range_element_type="TIMESTAMP", end=end_timestamp) + self.assertEqual(param.to_api_repr(), EXPECTED) + + def test_to_api_repr_w_timestamp_timestamp(self): + from google.cloud._helpers import UTC # type: ignore + + now = datetime.datetime.utcnow() + now = now.astimezone(UTC) + now_str = str(now) + EXPECTED = { + "parameterType": { + "type": "RANGE", + "rangeElementType": { + "type": "TIMESTAMP", + }, + }, + "parameterValue": { + "rangeValue": {"start": {"value": None}, "end": {"value": now_str}} + }, + } + klass = self._get_target_class() + param = klass.positional(range_element_type="TIMESTAMP", end=now) + self.assertEqual(param.to_api_repr(), EXPECTED) + + def test_to_api_repr_wo_values(self): + EXPECTED = { + "name": "foo", + "parameterType": { + "type": "RANGE", + "rangeElementType": { + "type": "DATE", + }, + }, + "parameterValue": { + "rangeValue": {"start": {"value": None}, "end": {"value": None}} + }, + } + param = self._make_one(range_element_type="DATE", name="foo") + self.assertEqual(param.to_api_repr(), EXPECTED) + + def test_to_api_repr_unsupported_value_type(self): + with self.assertRaisesRegex( + ValueError, "Cannot convert range element value from type" + ): + range_param = self._make_one( + range_element_type="DATE", start=datetime.date.today() + ) + range_param.range_element_type.type_._type = "LONG" + range_param.to_api_repr() + + def test___eq__(self): + param = self._make_one( + range_element_type="DATE", start="2016-08-11", name="foo" + ) + self.assertEqual(param, param) + self.assertNotEqual(param, object()) + alias = self._make_one( + range_element_type="DATE", start="2016-08-11", name="bar" + ) + self.assertNotEqual(param, alias) + wrong_type = self._make_one( + range_element_type="DATETIME", + start="2020-12-31 12:00:00.000000", + name="foo", + ) + self.assertNotEqual(param, wrong_type) + wrong_val = self._make_one( + range_element_type="DATE", start="2016-08-12", name="foo" + ) + self.assertNotEqual(param, wrong_val) + + def test___eq___wrong_type(self): + param = self._make_one( + range_element_type="DATE", start="2016-08-11", name="foo" + ) + other = object() + self.assertNotEqual(param, other) + self.assertEqual(param, mock.ANY) + + def test___eq___name_mismatch(self): + param = self._make_one( + range_element_type="DATE", start="2016-08-11", name="foo" + ) + other = self._make_one( + range_element_type="DATE", start="2016-08-11", name="bar" + ) + self.assertNotEqual(param, other) + + def test___eq___field_type_mismatch(self): + param = self._make_one(range_element_type="DATE") + other = self._make_one(range_element_type="DATETIME") + self.assertNotEqual(param, other) + + def test___eq___value_mismatch(self): + param = self._make_one(range_element_type="DATE", start="2016-08-11") + other = self._make_one(range_element_type="DATE", start="2016-08-12") + self.assertNotEqual(param, other) + + def test___eq___hit(self): + param = self._make_one(range_element_type="DATE", start="2016-08-12") + other = self._make_one(range_element_type="DATE", start="2016-08-12") + self.assertEqual(param, other) + + def test___ne___wrong_type(self): + param = self._make_one(range_element_type="DATE") + other = object() + self.assertNotEqual(param, other) + self.assertEqual(param, mock.ANY) + + def test___ne___same_value(self): + param1 = self._make_one(range_element_type="DATE") + param2 = self._make_one(range_element_type="DATE") + # unittest ``assertEqual`` uses ``==`` not ``!=``. + comparison_val = param1 != param2 + self.assertFalse(comparison_val) + + def test___ne___different_values(self): + param1 = self._make_one(range_element_type="DATE", start="2016-08-12") + param2 = self._make_one(range_element_type="DATE") + self.assertNotEqual(param1, param2) + + def test___repr__(self): + param1 = self._make_one(range_element_type="DATE", start="2016-08-12") + expected = "RangeQueryParameter(None, {'type': 'RANGE', 'rangeElementType': {'type': 'DATE'}}, '2016-08-12', None)" + self.assertEqual(repr(param1), expected) + + def _make_subparam(name, type_, value): from google.cloud.bigquery.query import ScalarQueryParameter