Skip to content

Commit 74e75e8

Browse files
authored
feat: support insertAll for range (#1909)
* feat: support insertAll for range * revert INTERVAL regex * lint * add unit test * lint
1 parent 0e39066 commit 74e75e8

File tree

2 files changed

+162
-4
lines changed

2 files changed

+162
-4
lines changed

google/cloud/bigquery/_helpers.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
r"(?P<days>-?\d+) "
5151
r"(?P<time_sign>-?)(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d+)\.?(?P<fraction>\d*)?$"
5252
)
53+
_RANGE_PATTERN = re.compile(r"\[.*, .*\)")
5354

5455
BIGQUERY_EMULATOR_HOST = "BIGQUERY_EMULATOR_HOST"
5556
"""Environment variable defining host for emulator."""
@@ -334,9 +335,8 @@ def _range_from_json(value, field):
334335
The parsed range object from ``value`` if the ``field`` is not
335336
null (otherwise it is :data:`None`).
336337
"""
337-
range_literal = re.compile(r"\[.*, .*\)")
338338
if _not_null(value, field):
339-
if range_literal.match(value):
339+
if _RANGE_PATTERN.match(value):
340340
start, end = value[1:-1].split(", ")
341341
start = _range_element_from_json(start, field.range_element_type)
342342
end = _range_element_from_json(end, field.range_element_type)
@@ -531,6 +531,52 @@ def _time_to_json(value):
531531
return value
532532

533533

534+
def _range_element_to_json(value, element_type=None):
535+
"""Coerce 'value' to an JSON-compatible representation."""
536+
if value is None:
537+
return None
538+
elif isinstance(value, str):
539+
if value.upper() in ("UNBOUNDED", "NULL"):
540+
return None
541+
else:
542+
# We do not enforce range element value to be valid to reduce
543+
# redundancy with backend.
544+
return value
545+
elif (
546+
element_type and element_type.element_type.upper() in _SUPPORTED_RANGE_ELEMENTS
547+
):
548+
converter = _SCALAR_VALUE_TO_JSON_ROW.get(element_type.element_type.upper())
549+
return converter(value)
550+
else:
551+
raise ValueError(
552+
f"Unsupported RANGE element type {element_type}, or "
553+
"element type is empty. Must be DATE, DATETIME, or "
554+
"TIMESTAMP"
555+
)
556+
557+
558+
def _range_field_to_json(range_element_type, value):
559+
"""Coerce 'value' to an JSON-compatible representation."""
560+
if isinstance(value, str):
561+
# string literal
562+
if _RANGE_PATTERN.match(value):
563+
start, end = value[1:-1].split(", ")
564+
else:
565+
raise ValueError(f"RANGE literal {value} has incorrect format")
566+
elif isinstance(value, dict):
567+
# dictionary
568+
start = value.get("start")
569+
end = value.get("end")
570+
else:
571+
raise ValueError(
572+
f"Unsupported type of RANGE value {value}, must be " "string or dict"
573+
)
574+
575+
start = _range_element_to_json(start, range_element_type)
576+
end = _range_element_to_json(end, range_element_type)
577+
return {"start": start, "end": end}
578+
579+
534580
# Converters used for scalar values marshalled to the BigQuery API, such as in
535581
# query parameters or the tabledata.insert API.
536582
_SCALAR_VALUE_TO_JSON_ROW = {
@@ -676,6 +722,8 @@ def _single_field_to_json(field, row_value):
676722

677723
if field.field_type == "RECORD":
678724
return _record_field_to_json(field.fields, row_value)
725+
if field.field_type == "RANGE":
726+
return _range_field_to_json(field.range_element_type, row_value)
679727

680728
return _scalar_field_to_json(field, row_value)
681729

tests/unit/test__helpers.py

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,10 +1049,22 @@ def test_w_datetime(self):
10491049
self.assertEqual(self._call_fut(when), "12:13:41")
10501050

10511051

1052-
def _make_field(field_type, mode="NULLABLE", name="testing", fields=()):
1052+
def _make_field(
1053+
field_type,
1054+
mode="NULLABLE",
1055+
name="testing",
1056+
fields=(),
1057+
range_element_type=None,
1058+
):
10531059
from google.cloud.bigquery.schema import SchemaField
10541060

1055-
return SchemaField(name=name, field_type=field_type, mode=mode, fields=fields)
1061+
return SchemaField(
1062+
name=name,
1063+
field_type=field_type,
1064+
mode=mode,
1065+
fields=fields,
1066+
range_element_type=range_element_type,
1067+
)
10561068

10571069

10581070
class Test_scalar_field_to_json(unittest.TestCase):
@@ -1251,6 +1263,98 @@ def test_w_dict_unknown_fields(self):
12511263
)
12521264

12531265

1266+
class Test_range_field_to_json(unittest.TestCase):
1267+
def _call_fut(self, field, value):
1268+
from google.cloud.bigquery._helpers import _range_field_to_json
1269+
1270+
return _range_field_to_json(field, value)
1271+
1272+
def test_w_date(self):
1273+
field = _make_field("RANGE", range_element_type="DATE")
1274+
start = datetime.date(2016, 12, 3)
1275+
original = {"start": start}
1276+
converted = self._call_fut(field.range_element_type, original)
1277+
expected = {"start": "2016-12-03", "end": None}
1278+
self.assertEqual(converted, expected)
1279+
1280+
def test_w_date_string(self):
1281+
field = _make_field("RANGE", range_element_type="DATE")
1282+
original = {"start": "2016-12-03"}
1283+
converted = self._call_fut(field.range_element_type, original)
1284+
expected = {"start": "2016-12-03", "end": None}
1285+
self.assertEqual(converted, expected)
1286+
1287+
def test_w_datetime(self):
1288+
field = _make_field("RANGE", range_element_type="DATETIME")
1289+
start = datetime.datetime(2016, 12, 3, 14, 11, 27, 123456)
1290+
original = {"start": start}
1291+
converted = self._call_fut(field.range_element_type, original)
1292+
expected = {"start": "2016-12-03T14:11:27.123456", "end": None}
1293+
self.assertEqual(converted, expected)
1294+
1295+
def test_w_datetime_string(self):
1296+
field = _make_field("RANGE", range_element_type="DATETIME")
1297+
original = {"start": "2016-12-03T14:11:27.123456"}
1298+
converted = self._call_fut(field.range_element_type, original)
1299+
expected = {"start": "2016-12-03T14:11:27.123456", "end": None}
1300+
self.assertEqual(converted, expected)
1301+
1302+
def test_w_timestamp(self):
1303+
from google.cloud._helpers import UTC
1304+
1305+
field = _make_field("RANGE", range_element_type="TIMESTAMP")
1306+
start = datetime.datetime(2016, 12, 3, 14, 11, 27, 123456, tzinfo=UTC)
1307+
original = {"start": start}
1308+
converted = self._call_fut(field.range_element_type, original)
1309+
expected = {"start": "2016-12-03T14:11:27.123456Z", "end": None}
1310+
self.assertEqual(converted, expected)
1311+
1312+
def test_w_timestamp_string(self):
1313+
field = _make_field("RANGE", range_element_type="TIMESTAMP")
1314+
original = {"start": "2016-12-03T14:11:27.123456Z"}
1315+
converted = self._call_fut(field.range_element_type, original)
1316+
expected = {"start": "2016-12-03T14:11:27.123456Z", "end": None}
1317+
self.assertEqual(converted, expected)
1318+
1319+
def test_w_timestamp_float(self):
1320+
field = _make_field("RANGE", range_element_type="TIMESTAMP")
1321+
original = {"start": 12.34567}
1322+
converted = self._call_fut(field.range_element_type, original)
1323+
expected = {"start": 12.34567, "end": None}
1324+
self.assertEqual(converted, expected)
1325+
1326+
def test_w_string_literal(self):
1327+
field = _make_field("RANGE", range_element_type="DATE")
1328+
original = "[2016-12-03, UNBOUNDED)"
1329+
converted = self._call_fut(field.range_element_type, original)
1330+
expected = {"start": "2016-12-03", "end": None}
1331+
self.assertEqual(converted, expected)
1332+
1333+
def test_w_unsupported_range_element_type(self):
1334+
field = _make_field("RANGE", range_element_type="TIME")
1335+
with self.assertRaises(ValueError):
1336+
self._call_fut(
1337+
field.range_element_type,
1338+
{"start": datetime.time(12, 13, 41)},
1339+
)
1340+
1341+
def test_w_no_range_element_type(self):
1342+
field = _make_field("RANGE")
1343+
with self.assertRaises(ValueError):
1344+
self._call_fut(field.range_element_type, "2016-12-03")
1345+
1346+
def test_w_incorrect_literal_format(self):
1347+
field = _make_field("RANGE", range_element_type="DATE")
1348+
original = "[2016-12-03, UNBOUNDED]"
1349+
with self.assertRaises(ValueError):
1350+
self._call_fut(field.range_element_type, original)
1351+
1352+
def test_w_unsupported_representation(self):
1353+
field = _make_field("RANGE", range_element_type="DATE")
1354+
with self.assertRaises(ValueError):
1355+
self._call_fut(field.range_element_type, object())
1356+
1357+
12541358
class Test_field_to_json(unittest.TestCase):
12551359
def _call_fut(self, field, value):
12561360
from google.cloud.bigquery._helpers import _field_to_json
@@ -1285,6 +1389,12 @@ def test_w_scalar(self):
12851389
converted = self._call_fut(field, original)
12861390
self.assertEqual(converted, str(original))
12871391

1392+
def test_w_range(self):
1393+
field = _make_field("RANGE", range_element_type="DATE")
1394+
original = {"start": "2016-12-03", "end": "2024-12-03"}
1395+
converted = self._call_fut(field, original)
1396+
self.assertEqual(converted, original)
1397+
12881398

12891399
class Test_snake_to_camel_case(unittest.TestCase):
12901400
def _call_fut(self, value):

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