|
19 | 19 | import decimal |
20 | 20 | import math |
21 | 21 | import re |
22 | | -from typing import Union |
| 22 | +from typing import Optional, Union |
23 | 23 |
|
| 24 | +from dateutil import relativedelta |
24 | 25 | from google.cloud._helpers import UTC |
25 | 26 | from google.cloud._helpers import _date_from_iso8601_date |
26 | 27 | from google.cloud._helpers import _datetime_from_microseconds |
|
40 | 41 | re.VERBOSE, |
41 | 42 | ) |
42 | 43 |
|
| 44 | +# BigQuery sends INTERVAL data in "canonical format" |
| 45 | +# https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#interval_type |
| 46 | +_INTERVAL_PATTERN = re.compile( |
| 47 | + r"(?P<calendar_sign>-?)(?P<years>\d+)-(?P<months>\d+) " |
| 48 | + r"(?P<days>-?\d+) " |
| 49 | + r"(?P<time_sign>-?)(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d+)\.?(?P<fraction>\d*)?$" |
| 50 | +) |
| 51 | + |
43 | 52 | _BQ_STORAGE_OPTIONAL_READ_SESSION_VERSION = packaging.version.Version("2.6.0") |
44 | 53 |
|
45 | 54 |
|
@@ -116,6 +125,41 @@ def _int_from_json(value, field): |
116 | 125 | return int(value) |
117 | 126 |
|
118 | 127 |
|
| 128 | +def _interval_from_json( |
| 129 | + value: Optional[str], field |
| 130 | +) -> Optional[relativedelta.relativedelta]: |
| 131 | + """Coerce 'value' to an interval, if set or not nullable.""" |
| 132 | + if not _not_null(value, field): |
| 133 | + return None |
| 134 | + if value is None: |
| 135 | + raise TypeError(f"got {value} for REQUIRED field: {repr(field)}") |
| 136 | + |
| 137 | + parsed = _INTERVAL_PATTERN.match(value) |
| 138 | + if parsed is None: |
| 139 | + raise ValueError(f"got interval: '{value}' with unexpected format") |
| 140 | + |
| 141 | + calendar_sign = -1 if parsed.group("calendar_sign") == "-" else 1 |
| 142 | + years = calendar_sign * int(parsed.group("years")) |
| 143 | + months = calendar_sign * int(parsed.group("months")) |
| 144 | + days = int(parsed.group("days")) |
| 145 | + time_sign = -1 if parsed.group("time_sign") == "-" else 1 |
| 146 | + hours = time_sign * int(parsed.group("hours")) |
| 147 | + minutes = time_sign * int(parsed.group("minutes")) |
| 148 | + seconds = time_sign * int(parsed.group("seconds")) |
| 149 | + fraction = parsed.group("fraction") |
| 150 | + microseconds = time_sign * int(fraction.ljust(6, "0")[:6]) if fraction else 0 |
| 151 | + |
| 152 | + return relativedelta.relativedelta( |
| 153 | + years=years, |
| 154 | + months=months, |
| 155 | + days=days, |
| 156 | + hours=hours, |
| 157 | + minutes=minutes, |
| 158 | + seconds=seconds, |
| 159 | + microseconds=microseconds, |
| 160 | + ) |
| 161 | + |
| 162 | + |
119 | 163 | def _float_from_json(value, field): |
120 | 164 | """Coerce 'value' to a float, if set or not nullable.""" |
121 | 165 | if _not_null(value, field): |
@@ -252,6 +296,7 @@ def _record_from_json(value, field): |
252 | 296 | _CELLDATA_FROM_JSON = { |
253 | 297 | "INTEGER": _int_from_json, |
254 | 298 | "INT64": _int_from_json, |
| 299 | + "INTERVAL": _interval_from_json, |
255 | 300 | "FLOAT": _float_from_json, |
256 | 301 | "FLOAT64": _float_from_json, |
257 | 302 | "NUMERIC": _decimal_from_json, |
|
0 commit comments