Skip to content
This repository was archived by the owner on Mar 31, 2026. It is now read-only.

Commit 3432870

Browse files
1 parent c992fb6 commit 3432870

File tree

3 files changed

+65
-44
lines changed

3 files changed

+65
-44
lines changed

google/cloud/spanner_v1/_helpers.py

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -201,21 +201,22 @@ def _datetime_to_rfc3339_nanoseconds(value):
201201
@dataclass
202202
class Interval:
203203
"""Represents a Spanner INTERVAL type.
204-
204+
205205
An interval is a combination of months, days and nanoseconds.
206206
Internally, Spanner supports Interval value with the following range of individual fields:
207207
months: [-120000, 120000]
208208
days: [-3660000, 3660000]
209209
nanoseconds: [-316224000000000000000, 316224000000000000000]
210210
"""
211+
211212
months: int = 0
212213
days: int = 0
213214
nanos: int = 0
214215

215216
def __str__(self) -> str:
216217
"""Returns the ISO8601 duration format string representation."""
217218
result = ["P"]
218-
219+
219220
# Handle years and months
220221
if self.months:
221222
is_negative = self.months < 0
@@ -225,43 +226,43 @@ def __str__(self) -> str:
225226
result.append(f"{'-' if is_negative else ''}{years}Y")
226227
if months:
227228
result.append(f"{'-' if is_negative else ''}{months}M")
228-
229+
229230
# Handle days
230231
if self.days:
231232
result.append(f"{self.days}D")
232-
233+
233234
# Handle time components
234235
if self.nanos:
235236
result.append("T")
236237
nanos = abs(self.nanos)
237238
is_negative = self.nanos < 0
238-
239+
239240
# Convert to hours, minutes, seconds
240241
nanos_per_hour = 3600000000000
241242
hours, nanos = divmod(nanos, nanos_per_hour)
242243
if hours:
243244
if is_negative:
244245
result.append("-")
245246
result.append(f"{hours}H")
246-
247+
247248
nanos_per_minute = 60000000000
248249
minutes, nanos = divmod(nanos, nanos_per_minute)
249250
if minutes:
250251
if is_negative:
251252
result.append("-")
252253
result.append(f"{minutes}M")
253-
254+
254255
nanos_per_second = 1000000000
255256
seconds, nanos_fraction = divmod(nanos, nanos_per_second)
256-
257+
257258
if seconds or nanos_fraction:
258259
if is_negative:
259260
result.append("-")
260261
if seconds:
261262
result.append(str(seconds))
262263
elif nanos_fraction:
263264
result.append("0")
264-
265+
265266
if nanos_fraction:
266267
nano_str = f"{nanos_fraction:09d}"
267268
trimmed = nano_str.rstrip("0")
@@ -276,54 +277,58 @@ def __str__(self) -> str:
276277
trimmed += "0"
277278
result.append(f".{trimmed}")
278279
result.append("S")
279-
280+
280281
if len(result) == 1:
281282
result.append("0Y") # Special case for zero interval
282-
283+
283284
return "".join(result)
284285

285286
@classmethod
286-
def from_str(cls, s: str) -> 'Interval':
287+
def from_str(cls, s: str) -> "Interval":
287288
"""Parse an ISO8601 duration format string into an Interval."""
288-
pattern = r'^P(-?\d+Y)?(-?\d+M)?(-?\d+D)?(T(-?\d+H)?(-?\d+M)?(-?((\d+([.,]\d{1,9})?)|([.,]\d{1,9}))S)?)?$'
289+
pattern = r"^P(-?\d+Y)?(-?\d+M)?(-?\d+D)?(T(-?\d+H)?(-?\d+M)?(-?((\d+([.,]\d{1,9})?)|([.,]\d{1,9}))S)?)?$"
289290
match = re.match(pattern, s)
290291
if not match or len(s) == 1:
291292
raise ValueError(f"Invalid interval format: {s}")
292-
293+
293294
parts = match.groups()
294295
if not any(parts[:3]) and not parts[3]:
295-
raise ValueError(f"Invalid interval format: at least one component (Y/M/D/H/M/S) is required: {s}")
296-
296+
raise ValueError(
297+
f"Invalid interval format: at least one component (Y/M/D/H/M/S) is required: {s}"
298+
)
299+
297300
if parts[3] == "T" and not any(parts[4:7]):
298-
raise ValueError(f"Invalid interval format: time designator 'T' present but no time components specified: {s}")
299-
301+
raise ValueError(
302+
f"Invalid interval format: time designator 'T' present but no time components specified: {s}"
303+
)
304+
300305
def parse_num(s: str, suffix: str) -> int:
301306
if not s:
302307
return 0
303308
return int(s.rstrip(suffix))
304-
309+
305310
years = parse_num(parts[0], "Y")
306311
months = parse_num(parts[1], "M")
307312
total_months = years * 12 + months
308-
313+
309314
days = parse_num(parts[2], "D")
310-
315+
311316
nanos = 0
312317
if parts[3]: # Has time component
313318
# Convert hours to nanoseconds
314319
hours = parse_num(parts[4], "H")
315320
nanos += hours * 3600000000000
316-
321+
317322
# Convert minutes to nanoseconds
318323
minutes = parse_num(parts[5], "M")
319324
nanos += minutes * 60000000000
320-
325+
321326
# Handle seconds and fractional seconds
322327
if parts[6]:
323328
seconds = parts[6].rstrip("S")
324329
if "," in seconds:
325330
seconds = seconds.replace(",", ".")
326-
331+
327332
if "." in seconds:
328333
sec_parts = seconds.split(".")
329334
whole_seconds = sec_parts[0] if sec_parts[0] else "0"
@@ -335,19 +340,20 @@ def parse_num(s: str, suffix: str) -> int:
335340
nanos += frac_nanos
336341
else:
337342
nanos += int(seconds) * 1000000000
338-
343+
339344
return cls(months=total_months, days=days, nanos=nanos)
340345

341346

342347
@dataclass
343348
class NullInterval:
344349
"""Represents a Spanner INTERVAL that may be NULL."""
350+
345351
interval: Interval
346352
valid: bool = True
347-
353+
348354
def is_null(self) -> bool:
349355
return not self.valid
350-
356+
351357
def __str__(self) -> str:
352358
if not self.valid:
353359
return "NULL"
@@ -641,7 +647,7 @@ def _parse_nullable(value_pb, decoder):
641647

642648
def _parse_interval(value_pb):
643649
"""Parse a Value protobuf containing an interval."""
644-
if hasattr(value_pb, 'string_value'):
650+
if hasattr(value_pb, "string_value"):
645651
return Interval.from_str(value_pb.string_value)
646652
return Interval.from_str(value_pb)
647653

tests/system/test_session_api.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2923,8 +2923,9 @@ def test_interval(sessions_database, database_dialect, not_emulator):
29232923

29242924
def setup_table():
29252925
if database_dialect == DatabaseDialect.POSTGRESQL:
2926-
sessions_database.update_ddl([
2927-
"""
2926+
sessions_database.update_ddl(
2927+
[
2928+
"""
29282929
CREATE TABLE IntervalTable (
29292930
key text primary key,
29302931
create_time timestamptz,
@@ -2933,10 +2934,12 @@ def setup_table():
29332934
interval_array_len bigint GENERATED ALWAYS AS (ARRAY_LENGTH(ARRAY[INTERVAL '1-2 3 4:5:6'], 1)) STORED
29342935
)
29352936
"""
2936-
]).result()
2937+
]
2938+
).result()
29372939
else:
2938-
sessions_database.update_ddl([
2939-
"""
2940+
sessions_database.update_ddl(
2941+
[
2942+
"""
29402943
CREATE TABLE IntervalTable (
29412944
key STRING(MAX),
29422945
create_time TIMESTAMP,
@@ -2945,10 +2948,13 @@ def setup_table():
29452948
interval_array_len INT64 AS (ARRAY_LENGTH(ARRAY<INTERVAL>[INTERVAL '1-2 3 4:5:6' YEAR TO SECOND]))
29462949
) PRIMARY KEY (key)
29472950
"""
2948-
]).result()
2951+
]
2952+
).result()
29492953

29502954
def insert_test1(transaction):
2951-
keys, placeholders = get_param_info(["key", "create_time", "expiry_time"], database_dialect)
2955+
keys, placeholders = get_param_info(
2956+
["key", "create_time", "expiry_time"], database_dialect
2957+
)
29522958
transaction.execute_sql(
29532959
f"""
29542960
INSERT INTO IntervalTable (key, create_time, expiry_time)
@@ -2998,7 +3004,9 @@ def test_interval_arithmetic(transaction):
29983004
assert interval.nanos == 0
29993005

30003006
def insert_test2(transaction):
3001-
keys, placeholders = get_param_info(["key", "create_time", "expiry_time"], database_dialect)
3007+
keys, placeholders = get_param_info(
3008+
["key", "create_time", "expiry_time"], database_dialect
3009+
)
30023010
transaction.execute_sql(
30033011
f"""
30043012
INSERT INTO IntervalTable (key, create_time, expiry_time)
@@ -3043,7 +3051,8 @@ def test_interval_array_param(transaction, database_dialect):
30433051
]
30443052
keys, placeholders = get_param_info(["intervals"], database_dialect)
30453053
array_type = spanner_v1.Type(
3046-
code=spanner_v1.TypeCode.ARRAY, array_element_type=spanner_v1.param_types.INTERVAL
3054+
code=spanner_v1.TypeCode.ARRAY,
3055+
array_element_type=spanner_v1.param_types.INTERVAL,
30473056
)
30483057
results = list(
30493058
transaction.execute_sql(
@@ -3056,23 +3065,23 @@ def test_interval_array_param(transaction, database_dialect):
30563065
row = results[0]
30573066
intervals = row[0]
30583067
assert len(intervals) == 4
3059-
3068+
30603069
# Check first interval
30613070
assert intervals[0].valid is True
30623071
assert intervals[0].interval.months == 14
30633072
assert intervals[0].interval.days == 3
30643073
assert intervals[0].interval.nanos == 14706000000000
3065-
3074+
30663075
assert intervals[1].valid is True
30673076
assert intervals[1].interval.months == 0
30683077
assert intervals[1].interval.days == 0
30693078
assert intervals[1].interval.nanos == 0
3070-
3079+
30713080
assert intervals[2].valid is True
30723081
assert intervals[2].interval.months == -14
30733082
assert intervals[2].interval.days == -3
30743083
assert intervals[2].interval.nanos == -14706000000000
3075-
3084+
30763085
assert intervals[3].valid is True
30773086
assert intervals[3].interval.months == 0
30783087
assert intervals[3].interval.days == 0
@@ -3094,14 +3103,14 @@ def test_interval_array_cast(transaction):
30943103
row = results[0]
30953104
intervals = row[0]
30963105
assert len(intervals) == 3
3097-
3106+
30983107
assert intervals[0].valid is True
30993108
assert intervals[0].interval.months == 14 # 1 year + 2 months
31003109
assert intervals[0].interval.days == 3
31013110
assert intervals[0].interval.nanos == 14706789123000 # 4h5m6.789123s in nanos
3102-
3111+
31033112
assert intervals[1].valid is False
3104-
3113+
31053114
assert intervals[2].valid is True
31063115
assert intervals[2].interval.months == -14
31073116
assert intervals[2].interval.days == -3

tests/unit/test__helpers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,7 @@ def _callFUT(self, *args, **kw):
12211221

12221222
def test_full_interval_with_all_components(self):
12231223
from google.protobuf.struct_pb2 import Value
1224+
12241225
input_str = "P1Y2M3DT12H12M6.789000123S"
12251226
expected_months = 14
12261227
expected_days = 3
@@ -1647,6 +1648,7 @@ def test_both_dot_and_comma_decimals(self):
16471648

16481649
def test_interval_with_years_months(self):
16491650
from google.protobuf.struct_pb2 import Value
1651+
16501652
input_str = "P1Y2M"
16511653
expected_months = 14
16521654
expected_days = 0
@@ -1659,6 +1661,7 @@ def test_interval_with_years_months(self):
16591661

16601662
def test_interval_with_days(self):
16611663
from google.protobuf.struct_pb2 import Value
1664+
16621665
input_str = "P3D"
16631666
expected_months = 0
16641667
expected_days = 3
@@ -1671,6 +1674,7 @@ def test_interval_with_days(self):
16711674

16721675
def test_interval_with_time(self):
16731676
from google.protobuf.struct_pb2 import Value
1677+
16741678
input_str = "PT12H12M6.789000123S"
16751679
expected_months = 0
16761680
expected_days = 0
@@ -1683,6 +1687,7 @@ def test_interval_with_time(self):
16831687

16841688
def test_interval_with_negative_components(self):
16851689
from google.protobuf.struct_pb2 import Value
1690+
16861691
input_str = "P-1Y-2M-3DT-12H-12M-6.789000123S"
16871692
expected_months = -14
16881693
expected_days = -3
@@ -1695,6 +1700,7 @@ def test_interval_with_negative_components(self):
16951700

16961701
def test_interval_with_zero_components(self):
16971702
from google.protobuf.struct_pb2 import Value
1703+
16981704
input_str = "P0Y0M0DT0H0M0S"
16991705
expected_months = 0
17001706
expected_days = 0

0 commit comments

Comments
 (0)