Skip to content

Commit 86a45c9

Browse files
authored
feat: support range sql (#1807)
* feat: support range sql * add unit tests * add system test * lint and remove debug code * lint and remove debug code * remove added blank line * add comment for legacy type
1 parent 53c2cbf commit 86a45c9

File tree

4 files changed

+127
-1
lines changed

4 files changed

+127
-1
lines changed

google/cloud/bigquery/enums.py

+2
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ def _generate_next_value_(name, start, count, last_values):
254254
JSON = enum.auto()
255255
ARRAY = enum.auto()
256256
STRUCT = enum.auto()
257+
RANGE = enum.auto()
257258

258259

259260
class EntityTypes(str, enum.Enum):
@@ -292,6 +293,7 @@ class SqlTypeNames(str, enum.Enum):
292293
TIME = "TIME"
293294
DATETIME = "DATETIME"
294295
INTERVAL = "INTERVAL" # NOTE: not available in legacy types
296+
RANGE = "RANGE" # NOTE: not available in legacy types
295297

296298

297299
class WriteDisposition(object):

google/cloud/bigquery/standard_sql.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class StandardSqlDataType:
4343
]
4444
}
4545
}
46+
RANGE: {type_kind="RANGE", range_element_type="DATETIME"}
4647
4748
Args:
4849
type_kind:
@@ -52,6 +53,8 @@ class StandardSqlDataType:
5253
The type of the array's elements, if type_kind is ARRAY.
5354
struct_type:
5455
The fields of this struct, in order, if type_kind is STRUCT.
56+
range_element_type:
57+
The type of the range's elements, if type_kind is RANGE.
5558
"""
5659

5760
def __init__(
@@ -61,12 +64,14 @@ def __init__(
6164
] = StandardSqlTypeNames.TYPE_KIND_UNSPECIFIED,
6265
array_element_type: Optional["StandardSqlDataType"] = None,
6366
struct_type: Optional["StandardSqlStructType"] = None,
67+
range_element_type: Optional["StandardSqlDataType"] = None,
6468
):
6569
self._properties: Dict[str, Any] = {}
6670

6771
self.type_kind = type_kind
6872
self.array_element_type = array_element_type
6973
self.struct_type = struct_type
74+
self.range_element_type = range_element_type
7075

7176
@property
7277
def type_kind(self) -> Optional[StandardSqlTypeNames]:
@@ -127,6 +132,28 @@ def struct_type(self, value: Optional["StandardSqlStructType"]):
127132
else:
128133
self._properties["structType"] = struct_type
129134

135+
@property
136+
def range_element_type(self) -> Optional["StandardSqlDataType"]:
137+
"""The type of the range's elements, if type_kind = "RANGE". Must be
138+
one of DATETIME, DATE, or TIMESTAMP."""
139+
range_element_info = self._properties.get("rangeElementType")
140+
141+
if range_element_info is None:
142+
return None
143+
144+
result = StandardSqlDataType()
145+
result._properties = range_element_info # We do not use a copy on purpose.
146+
return result
147+
148+
@range_element_type.setter
149+
def range_element_type(self, value: Optional["StandardSqlDataType"]):
150+
range_element_type = None if value is None else value.to_api_repr()
151+
152+
if range_element_type is None:
153+
self._properties.pop("rangeElementType", None)
154+
else:
155+
self._properties["rangeElementType"] = range_element_type
156+
130157
def to_api_repr(self) -> Dict[str, Any]:
131158
"""Construct the API resource representation of this SQL data type."""
132159
return copy.deepcopy(self._properties)
@@ -155,7 +182,13 @@ def from_api_repr(cls, resource: Dict[str, Any]):
155182
if struct_info:
156183
struct_type = StandardSqlStructType.from_api_repr(struct_info)
157184

158-
return cls(type_kind, array_element_type, struct_type)
185+
range_element_type = None
186+
if type_kind == StandardSqlTypeNames.RANGE:
187+
range_element_info = resource.get("rangeElementType")
188+
if range_element_info:
189+
range_element_type = cls.from_api_repr(range_element_info)
190+
191+
return cls(type_kind, array_element_type, struct_type, range_element_type)
159192

160193
def __eq__(self, other):
161194
if not isinstance(other, StandardSqlDataType):
@@ -165,6 +198,7 @@ def __eq__(self, other):
165198
self.type_kind == other.type_kind
166199
and self.array_element_type == other.array_element_type
167200
and self.struct_type == other.struct_type
201+
and self.range_element_type == other.range_element_type
168202
)
169203

170204
def __str__(self):

tests/system/test_client.py

+38
Original file line numberDiff line numberDiff line change
@@ -2193,6 +2193,44 @@ def test_create_routine(self):
21932193
assert len(rows) == 1
21942194
assert rows[0].max_value == 100.0
21952195

2196+
def test_create_routine_with_range(self):
2197+
routine_name = "routine_range"
2198+
dataset = self.temp_dataset(_make_dataset_id("routine_range"))
2199+
2200+
routine = bigquery.Routine(
2201+
dataset.routine(routine_name),
2202+
type_="SCALAR_FUNCTION",
2203+
language="SQL",
2204+
body="RANGE_START(x)",
2205+
arguments=[
2206+
bigquery.RoutineArgument(
2207+
name="x",
2208+
data_type=bigquery.StandardSqlDataType(
2209+
type_kind=bigquery.StandardSqlTypeNames.RANGE,
2210+
range_element_type=bigquery.StandardSqlDataType(
2211+
type_kind=bigquery.StandardSqlTypeNames.DATE
2212+
),
2213+
),
2214+
)
2215+
],
2216+
return_type=bigquery.StandardSqlDataType(
2217+
type_kind=bigquery.StandardSqlTypeNames.DATE
2218+
),
2219+
)
2220+
2221+
query_string = (
2222+
"SELECT `{}`(RANGE<DATE> '[2016-08-12, UNBOUNDED)') as range_start;".format(
2223+
str(routine.reference)
2224+
)
2225+
)
2226+
2227+
routine = helpers.retry_403(Config.CLIENT.create_routine)(routine)
2228+
query_job = helpers.retry_403(Config.CLIENT.query)(query_string)
2229+
rows = list(query_job.result())
2230+
2231+
assert len(rows) == 1
2232+
assert rows[0].range_start == datetime.date(2016, 8, 12)
2233+
21962234
def test_create_tvf_routine(self):
21972235
from google.cloud.bigquery import (
21982236
Routine,

tests/unit/test_standard_sql_types.py

+52
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,28 @@ def test_to_api_repr_struct_type_w_field_types(self):
129129
}
130130
assert result == expected
131131

132+
def test_to_api_repr_range_type_element_type_missing(self):
133+
instance = self._make_one(
134+
bq.StandardSqlTypeNames.RANGE, range_element_type=None
135+
)
136+
137+
result = instance.to_api_repr()
138+
139+
assert result == {"typeKind": "RANGE"}
140+
141+
def test_to_api_repr_range_type_w_element_type(self):
142+
range_element_type = self._make_one(type_kind=bq.StandardSqlTypeNames.DATE)
143+
instance = self._make_one(
144+
bq.StandardSqlTypeNames.RANGE, range_element_type=range_element_type
145+
)
146+
147+
result = instance.to_api_repr()
148+
149+
assert result == {
150+
"typeKind": "RANGE",
151+
"rangeElementType": {"typeKind": "DATE"},
152+
}
153+
132154
def test_from_api_repr_empty_resource(self):
133155
klass = self._get_target_class()
134156
result = klass.from_api_repr(resource={})
@@ -276,6 +298,31 @@ def test_from_api_repr_struct_type_incomplete_field_info(self):
276298
)
277299
assert result == expected
278300

301+
def test_from_api_repr_range_type_full(self):
302+
klass = self._get_target_class()
303+
resource = {"typeKind": "RANGE", "rangeElementType": {"typeKind": "DATE"}}
304+
305+
result = klass.from_api_repr(resource=resource)
306+
307+
expected = klass(
308+
type_kind=bq.StandardSqlTypeNames.RANGE,
309+
range_element_type=klass(type_kind=bq.StandardSqlTypeNames.DATE),
310+
)
311+
assert result == expected
312+
313+
def test_from_api_repr_range_type_missing_element_type(self):
314+
klass = self._get_target_class()
315+
resource = {"typeKind": "RANGE"}
316+
317+
result = klass.from_api_repr(resource=resource)
318+
319+
expected = klass(
320+
type_kind=bq.StandardSqlTypeNames.RANGE,
321+
range_element_type=None,
322+
struct_type=None,
323+
)
324+
assert result == expected
325+
279326
def test__eq__another_type(self):
280327
instance = self._make_one()
281328

@@ -321,6 +368,11 @@ def test__eq__similar_instance(self):
321368
bq.StandardSqlStructType(fields=[bq.StandardSqlField(name="foo")]),
322369
bq.StandardSqlStructType(fields=[bq.StandardSqlField(name="bar")]),
323370
),
371+
(
372+
"range_element_type",
373+
bq.StandardSqlDataType(type_kind=bq.StandardSqlTypeNames.DATE),
374+
bq.StandardSqlDataType(type_kind=bq.StandardSqlTypeNames.DATETIME),
375+
),
324376
),
325377
)
326378
def test__eq__attribute_differs(self, attr_name, value, value2):

0 commit comments

Comments
 (0)