Skip to content

Commit fc4442e

Browse files
committed
Send TIMESTAMP query parameters as string.
- *Not* the float-time-since-epoch-in-seconds which Bigquery uses for all other TIMESTAMP values. :( - *Not* RFC3339, but the SQL-mandated format with an embedded space. :( Closes: #2886.
1 parent 3d354dc commit fc4442e

3 files changed

Lines changed: 49 additions & 13 deletions

File tree

bigquery/google/cloud/bigquery/_helpers.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
from collections import OrderedDict
1919
import datetime
2020

21+
from google.cloud._helpers import UTC
2122
from google.cloud._helpers import _date_from_iso8601_date
2223
from google.cloud._helpers import _datetime_from_microseconds
2324
from google.cloud._helpers import _datetime_to_rfc3339
24-
from google.cloud._helpers import _microseconds_from_datetime
2525
from google.cloud._helpers import _RFC3339_NO_FRACTION
2626
from google.cloud._helpers import _time_from_iso8601_time_naive
2727
from google.cloud._helpers import _to_bytes
@@ -150,7 +150,11 @@ def _bytes_to_json(value):
150150
def _timestamp_to_json(value):
151151
"""Coerce 'value' to an JSON-compatible representation."""
152152
if isinstance(value, datetime.datetime):
153-
value = _microseconds_from_datetime(value) / 1.0e6
153+
if value.tzinfo not in (None, UTC):
154+
# Convert to UTC and remove the time zone info.
155+
value = value.replace(tzinfo=None) - value.utcoffset()
156+
value = '%s %s+00:00' % (
157+
value.date().isoformat(), value.time().isoformat())
154158
return value
155159

156160

bigquery/unit_tests/test__helpers.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -546,13 +546,35 @@ def _call_fut(self, value):
546546
def test_w_float(self):
547547
self.assertEqual(self._call_fut(1.234567), 1.234567)
548548

549-
def test_w_datetime(self):
549+
def test_w_string(self):
550+
ZULU = '2016-12-20 15:58:27.339328+00:00'
551+
self.assertEqual(self._call_fut(ZULU), ZULU)
552+
553+
def test_w_datetime_wo_zone(self):
554+
import datetime
555+
ZULU = '2016-12-20 15:58:27.339328+00:00'
556+
when = datetime.datetime(2016, 12, 20, 15, 58, 27, 339328)
557+
self.assertEqual(self._call_fut(when), ZULU)
558+
559+
def test_w_datetime_w_non_utc_zone(self):
560+
import datetime
561+
562+
class _Zone(datetime.tzinfo):
563+
564+
def utcoffset(self, _):
565+
return datetime.timedelta(minutes=-240)
566+
567+
ZULU = '2016-12-20 19:58:27.339328+00:00'
568+
when = datetime.datetime(
569+
2016, 12, 20, 15, 58, 27, 339328, tzinfo=_Zone())
570+
self.assertEqual(self._call_fut(when), ZULU)
571+
572+
def test_w_datetime_w_utc_zone(self):
550573
import datetime
551574
from google.cloud._helpers import UTC
552-
from google.cloud._helpers import _microseconds_from_datetime
553-
when = datetime.datetime(2016, 12, 3, 14, 11, 27, tzinfo=UTC)
554-
self.assertEqual(self._call_fut(when),
555-
_microseconds_from_datetime(when) / 1e6)
575+
ZULU = '2016-12-20 15:58:27.339328+00:00'
576+
when = datetime.datetime(2016, 12, 20, 15, 58, 27, 339328, tzinfo=UTC)
577+
self.assertEqual(self._call_fut(when), ZULU)
556578

557579

558580
class Test_datetime_to_json(unittest.TestCase):
@@ -907,20 +929,20 @@ def test_to_api_repr_w_bool(self):
907929
self.assertEqual(param.to_api_repr(), EXPECTED)
908930

909931
def test_to_api_repr_w_timestamp_datetime(self):
932+
from google.cloud._helpers import UTC
910933
import datetime
911-
from google.cloud._helpers import _microseconds_from_datetime
912-
now = datetime.datetime.utcnow()
913-
seconds = _microseconds_from_datetime(now) / 1.0e6
934+
STAMP = '2016-12-20 15:58:27.339328+00:00'
935+
when = datetime.datetime(2016, 12, 20, 15, 58, 27, 339328, tzinfo=UTC)
914936
EXPECTED = {
915937
'parameterType': {
916938
'type': 'TIMESTAMP',
917939
},
918940
'parameterValue': {
919-
'value': seconds,
941+
'value': STAMP,
920942
},
921943
}
922944
klass = self._get_target_class()
923-
param = klass.positional(type_='TIMESTAMP', value=now)
945+
param = klass.positional(type_='TIMESTAMP', value=when)
924946
self.assertEqual(param.to_api_repr(), EXPECTED)
925947

926948
def test_to_api_repr_w_timestamp_micros(self):

system_tests/bigquery.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,9 +482,12 @@ def _job_done(instance):
482482
def test_sync_query_w_standard_sql_types(self):
483483
import datetime
484484
from google.cloud._helpers import UTC
485+
from google.cloud.bigquery._helpers import ScalarQueryParameter
485486
naive = datetime.datetime(2016, 12, 5, 12, 41, 9)
486487
stamp = "%s %s" % (naive.date().isoformat(), naive.time().isoformat())
487488
zoned = naive.replace(tzinfo=UTC)
489+
zoned_param = ScalarQueryParameter(
490+
name='zoned', type_='TIMESTAMP', value=zoned)
488491
EXAMPLES = [
489492
{
490493
'sql': 'SELECT 1',
@@ -553,9 +556,16 @@ def test_sync_query_w_standard_sql_types(self):
553556
'sql': 'SELECT ARRAY(SELECT STRUCT([1, 2]))',
554557
'expected': [{u'_field_1': [1, 2]}],
555558
},
559+
{
560+
'sql': 'SELECT @zoned',
561+
'expected': zoned,
562+
'query_parameters': [zoned_param],
563+
},
556564
]
557565
for example in EXAMPLES:
558-
query = Config.CLIENT.run_sync_query(example['sql'])
566+
query = Config.CLIENT.run_sync_query(
567+
example['sql'],
568+
query_parameters=example.get('query_parameters', ()))
559569
query.use_legacy_sql = False
560570
query.run()
561571
self.assertEqual(len(query.rows), 1)

0 commit comments

Comments
 (0)