Skip to content

Commit 4cea949

Browse files
authored
BigQuery: NUMERIC type support (googleapis#5331)
* Support for BigQuery's NUMERIC type, which is currently in alpha. (googleapis#4874) * Support for BigQuery's NUMERIC type, which is currently in alpha. * Remove unused import from test_query.py. * Fix newly-added system tests; all system and unit tests pass now. Add unit tests to reach what should be 100% coverage for new code. * Fix lint warning and rename shadowed unit test. * Add unit test that NUMERIC types are encoded correctly in insert_rows * Convert numeric unit test to new mocked connection style. * use var for numeric type in SQL system test
1 parent 28653c0 commit 4cea949

13 files changed

Lines changed: 166 additions & 14 deletions

File tree

bigquery/google/cloud/bigquery/_helpers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import base64
1818
import datetime
19+
import decimal
1920

2021
from google.api_core import retry
2122
from google.cloud._helpers import UTC
@@ -46,6 +47,12 @@ def _float_from_json(value, field):
4647
return float(value)
4748

4849

50+
def _decimal_from_json(value, field):
51+
"""Coerce 'value' to a Decimal, if set or not nullable."""
52+
if _not_null(value, field):
53+
return decimal.Decimal(value)
54+
55+
4956
def _bool_from_json(value, field):
5057
"""Coerce 'value' to a bool, if set or not nullable."""
5158
if _not_null(value, field):
@@ -160,6 +167,7 @@ def _record_from_json(value, field):
160167
'INT64': _int_from_json,
161168
'FLOAT': _float_from_json,
162169
'FLOAT64': _float_from_json,
170+
'NUMERIC': _decimal_from_json,
163171
'BOOLEAN': _bool_from_json,
164172
'BOOL': _bool_from_json,
165173
'STRING': _string_from_json,
@@ -228,6 +236,13 @@ def _float_to_json(value):
228236
return value
229237

230238

239+
def _decimal_to_json(value):
240+
"""Coerce 'value' to a JSON-compatible representation."""
241+
if isinstance(value, decimal.Decimal):
242+
value = str(value)
243+
return value
244+
245+
231246
def _bool_to_json(value):
232247
"""Coerce 'value' to an JSON-compatible representation."""
233248
if isinstance(value, bool):
@@ -293,6 +308,7 @@ def _time_to_json(value):
293308
'INT64': _int_to_json,
294309
'FLOAT': _float_to_json,
295310
'FLOAT64': _float_to_json,
311+
'NUMERIC': _decimal_to_json,
296312
'BOOLEAN': _bool_to_json,
297313
'BOOL': _bool_to_json,
298314
'BYTES': _bytes_to_json,

bigquery/google/cloud/bigquery/dbapi/_helpers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import collections
1616
import datetime
17+
import decimal
1718
import numbers
1819

1920
import six
@@ -46,6 +47,8 @@ def scalar_to_query_parameter(value, name=None):
4647
parameter_type = 'INT64'
4748
elif isinstance(value, numbers.Real):
4849
parameter_type = 'FLOAT64'
50+
elif isinstance(value, decimal.Decimal):
51+
parameter_type = 'NUMERIC'
4952
elif isinstance(value, six.text_type):
5053
parameter_type = 'STRING'
5154
elif isinstance(value, six.binary_type):

bigquery/google/cloud/bigquery/dbapi/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,6 @@ def __eq__(self, other):
7979
STRING = 'STRING'
8080
BINARY = _DBAPITypeObject('BYTES', 'RECORD', 'STRUCT')
8181
NUMBER = _DBAPITypeObject(
82-
'INTEGER', 'INT64', 'FLOAT', 'FLOAT64', 'BOOLEAN', 'BOOL')
82+
'INTEGER', 'INT64', 'FLOAT', 'FLOAT64', 'NUMERIC', 'BOOLEAN', 'BOOL')
8383
DATETIME = _DBAPITypeObject('TIMESTAMP', 'DATE', 'TIME', 'DATETIME')
8484
ROWID = 'ROWID'

bigquery/google/cloud/bigquery/query.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,11 @@ class ScalarQueryParameter(_AbstractQueryParameter):
8181
8282
:type type_: str
8383
:param type_: name of parameter type. One of 'STRING', 'INT64',
84-
'FLOAT64', 'BOOL', 'TIMESTAMP', 'DATETIME', or 'DATE'.
84+
'FLOAT64', 'NUMERIC', 'BOOL', 'TIMESTAMP', 'DATETIME', or
85+
'DATE'.
8586
86-
:type value: str, int, float, bool, :class:`datetime.datetime`, or
87-
:class:`datetime.date`.
87+
:type value: str, int, float, :class:`decimal.Decimal`, bool,
88+
:class:`datetime.datetime`, or :class:`datetime.date`.
8889
:param value: the scalar parameter value.
8990
"""
9091
def __init__(self, name, type_, value):
@@ -99,9 +100,11 @@ def positional(cls, type_, value):
99100
:type type_: str
100101
:param type_:
101102
name of parameter type. One of 'STRING', 'INT64',
102-
'FLOAT64', 'BOOL', 'TIMESTAMP', 'DATETIME', or 'DATE'.
103+
'FLOAT64', 'NUMERIC', 'BOOL', 'TIMESTAMP', 'DATETIME', or
104+
'DATE'.
103105
104-
:type value: str, int, float, bool, :class:`datetime.datetime`, or
106+
:type value: str, int, float, :class:`decimal.Decimal`, bool,
107+
:class:`datetime.datetime`, or
105108
:class:`datetime.date`.
106109
:param value: the scalar parameter value.
107110
@@ -185,7 +188,7 @@ class ArrayQueryParameter(_AbstractQueryParameter):
185188
:type array_type: str
186189
:param array_type:
187190
name of type of array elements. One of `'STRING'`, `'INT64'`,
188-
`'FLOAT64'`, `'BOOL'`, `'TIMESTAMP'`, or `'DATE'`.
191+
`'FLOAT64'`, `'NUMERIC'`, `'BOOL'`, `'TIMESTAMP'`, or `'DATE'`.
189192
190193
:type values: list of appropriate scalar type.
191194
:param values: the parameter array values.
@@ -202,7 +205,7 @@ def positional(cls, array_type, values):
202205
:type array_type: str
203206
:param array_type:
204207
name of type of array elements. One of `'STRING'`, `'INT64'`,
205-
`'FLOAT64'`, `'BOOL'`, `'TIMESTAMP'`, or `'DATE'`.
208+
`'FLOAT64'`, `'NUMERIC'`, `'BOOL'`, `'TIMESTAMP'`, or `'DATE'`.
206209
207210
:type values: list of appropriate scalar type
208211
:param values: the parameter array values.

bigquery/google/cloud/bigquery/schema.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ class SchemaField(object):
2323
2424
:type field_type: str
2525
:param field_type: the type of the field (one of 'STRING', 'INTEGER',
26-
'FLOAT', 'BOOLEAN', 'TIMESTAMP' or 'RECORD').
26+
'FLOAT', 'NUMERIC', 'BOOLEAN', 'TIMESTAMP' or
27+
'RECORD').
2728
2829
:type mode: str
2930
:param mode: the mode of the field (one of 'NULLABLE', 'REQUIRED',
@@ -77,8 +78,8 @@ def name(self):
7778
def field_type(self):
7879
"""str: The type of the field.
7980
80-
Will be one of 'STRING', 'INTEGER', 'FLOAT', 'BOOLEAN',
81-
'TIMESTAMP' or 'RECORD'.
81+
Will be one of 'STRING', 'INTEGER', 'FLOAT', 'NUMERIC',
82+
'BOOLEAN', 'TIMESTAMP' or 'RECORD'.
8283
"""
8384
return self._field_type
8485

bigquery/tests/data/characters.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"TeaTime" : "15:00:00",
3939
"Weight" : 198.6,
4040
"FavoriteTime" : "2001-12-19T23:59:59",
41+
"FavoriteNumber" : "3.141592654",
4142
"IsMagic" : true
4243
},
4344
{
@@ -47,6 +48,7 @@
4748
"IsMagic" : true,
4849
"FavoriteTime" : "2000-10-31T23:27:46",
4950
"Age" : "17",
51+
"FavoriteNumber" : "13",
5052
"Spells" : [
5153
{
5254
"LastUsed" : "2017-02-14 12:07:23 UTC",
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
{"Name":"Bilbo","Age":"111","Weight":67.2,"IsMagic":false,"Spells":[],"TeaTime":"10:00:00","NextVacation":"2017-09-22","FavoriteTime":"2031-04-01T05:09:27"}
2-
{"Name":"Gandalf","Age":"1000","Weight":198.6,"IsMagic":true,"Spells":[{"Name": "Skydragon", "Icon":"iVBORw0KGgoAAAANSUhEUgAAAB4AAAAgCAYAAAAFQMh/AAAAAXNSR0IArs4c6QAAA9lJREFUSA21lk9OVEEQxvsRDImoiMG9mLjjCG5mEg7gEfQGsIcF7p0EDsBBSJiNO7ZsFRZqosb/QkSj7fer7ur33sw8GDFUUq+7q6vqq6qu7pkQzqG4EeI521e7FePVgM9cGPYwhCi6UO8qFOK+YY+Br66ujsmmxb84Yzwp6zCsxjJfWVkxnMsEMGuWHZ9Wcz11cM48hkq0vLwc1tbW4mAwqDpcdIqnMmgF0JMv2CiGnZ2dcHR0FA4PD8Pe3t5U/tx6bCSlb+JT8XfxT3HsUek0Li0tRdjWl+z6iRF+FNA1hXPDQ/IMNyRg3s8bD/OaZS+VP+9cOLSa64cA34oXZWagDkRzAaJxXaE+ufc4rCN7LrazZ2+8+STtpAL8WYDvpTaHKlkB2iQARMvb2+H27m4YaL7zaDtUw1BZAASi6T8T2UZnPZV2pvnJfCH5p8bewcGB6TrIfz8wBZgHQ83kjpuj6RBYQpuo09Tvmpd7TPe+ktZN8cKwS92KWXGuaqWowlYEwthtMcWOZUNJc8at+zuF/Xkqo69baS7P+AvWjYwJ4jyHXXsEnd74ZO/Pq+uXUuv6WNlso6cvnDsZB1V/unJab3D1/KrJDw9NCM9wHf2FK2ejTKMejnBHfGtfH7LGGCdQDqaqJgfgzWjXK1nYV4jRbPGnxUT7cqUaZfJrVZeOm9QmB21L6xXgbu/ScsYusJFMoU0x2fsamRJOd6kOYDRLUxv94ENZe8+0gM+0dyz+KgU7X8rLHHCIOZyrna4y6ykIu0YCs02TBXmk3PZssmEgaTxTo83xjCIjoE21h0Yah3MrV4+9kR8MaabGze+9NEILGAFE5nMOiiA32KnAr/sb7tED3nzlzC4dB38WMC+EjaqHfqvUKHi2gJPdWQ6AbH8hgyQ7QY6jvjj3QZWvX6pUAtduTX5Dss96Q7NI9RQRJeeKvRFbt0v2gb1Gx/PooJsztn1c1DqpAU3Hde2dB2aEHBhjgOFjMeDvxLafjQ3YZQSgOcHJZX611H45sGLHWvYTz9hiURlpNoBZvxb/Ft9lAQ1DmBfUiR+j1hAPkMBTE9L9+zLva1QvGFHurRBaZ5xLVitoBviiRkD/sIMDztKA5FA0b9/0OclzO2/XAQymJ0TcghZwEo9/AX8gMeAJMOvIsWWt5bwCoiFhVSllrdH0t5Q1JHAFlKJNkvTVdn2GHb9KdmacMT+d/Os05imJUccRX2YuZ93Sxf0Ilc4DPDeAq5SAvFEAY94cQc6BA26dzb4HWAJI4DPmQE5KCVUyvb2FcDZem7JdT2ggKUP3xX6n9XNq1DpzSf4Cy4ZqSlmM8d8AAAAASUVORK5CYII=","DiscoveredBy":"Firebreather","Properties":[{"Name":"Flying","Power":1},{"Name":"Creature","Power":1},{"Name":"Explodey","Power":11}],"LastUsed":"2015-10-31 23:59:56 UTC"}],"TeaTime":"15:00:00","NextVacation":"2666-06-06","FavoriteTime":"2001-12-19T23:59:59"}
3-
{"Name":"Sabrina","Age":"17","Weight":128.3,"IsMagic":true,"Spells":[{"Name": "Talking cats", "Icon":"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAABxpRE9UAAAAAgAAAAAAAAAgAAAAKAAAACAAAAAgAAABxj2CfowAAAGSSURBVHgB7Jc9TsNAEIX3JDkCPUV6KlpKFHEGlD4nyA04ACUXQKTgCEipUnKGNEbP0otentayicZ24SlWs7tjO/N9u/5J2b2+NUtuZcnwYE8BuQPyGZAPwXwLLPk5kG+BJa9+fgfkh1B+CeancL4F8i2Q/wWm/S/w+XFoTseftn0dvhu0OXfhpM+AGvzcEiYVAFisPqE9zrETJhHAlXfg2lglMK9z0f3RBfB+ZyRUV3x+erzsEIjjOBqc1xtNAIrvguybV3A9lkVHxlEE6GrrPb/ZvAySwlUnfCmlPQ+R8JCExvGtcRQBLFwj4FGkznX1VYDKPG/f2/MjwCksXACgdNUxJjwK9xwl4JihOwTFR0kIF+CABEPRnvsvPFctMoYKqAFSAFaMwB4pp3Y+bodIYL9WmIAaIOHxo7W8wiHvAjTvhUeNwwSgeAeAABbqOewC5hBdwFD4+9+7puzXV9fS6/b1wwT4tsaYAhwOOQdUQch5vgZCeAhAv3ZM31yYAAUgvApQQQ6n5w6FB/RVe1jdJOAPAAD//1eMQwoAAAGQSURBVO1UMU4DQQy8X9AgWopIUINEkS4VlJQo4gvwAV7AD3gEH4iSgidESpWSXyyZExP5lr0c7K5PsXBhec/2+jzjuWtent9CLdtu1mG5+gjz+WNr7IsY7eH+tvO+xfuqk4vz7CH91edFaF5v9nb6dBKm13edvrL+0Lk5lMzJkQDeJSkkgHF6mR8CHwMHCQR/NAQQGD0BAlwK4FCefQiefq+A2Vn29tG7igLAfmwcnJu/nJy3BMQkMN9HEPr8AL3bfBv7Bp+7/SoExMDjZwKEJwmyhnnmQIQEBIlz2x0iKoAvJkAC6TsTIH6MqRrEWUMSZF2zAwqT4Eu/e6pzFAIkmNSZ4OFT+VYBIIF//UqbJwnF/4DU0GwOn8r/JQYCpPGufEfJuZiA37ycQw/5uFeqPq4pfR6FADmkBCXjfWdZj3NfXW58dAJyB9W65wRoMWulryvAyqa05nQFaDFrpa8rwMqmtOZ0BWgxa6WvK8DKprTmdAVoMWulryvAyqa05nQFaDFrpa8rwMqmtOb89wr4AtQ4aPoL6yVpAAAAAElFTkSuQmCC","DiscoveredBy":"Salem","Properties":[{"Name":"Makes you look crazy","Power":1}],"LastUsed":"2017-02-14 12:07:23 UTC"}],"TeaTime":"12:00:00","NextVacation":"2017-03-14","FavoriteTime":"2000-10-31T23:27:46"}
1+
{"Name":"Bilbo","Age":"111","Weight":67.2,"IsMagic":false,"Spells":[],"TeaTime":"10:00:00","NextVacation":"2017-09-22","FavoriteTime":"2031-04-01T05:09:27","FavoriteNumber":"111"}
2+
{"Name":"Gandalf","Age":"1000","Weight":198.6,"IsMagic":true,"Spells":[{"Name": "Skydragon", "Icon":"iVBORw0KGgoAAAANSUhEUgAAAB4AAAAgCAYAAAAFQMh/AAAAAXNSR0IArs4c6QAAA9lJREFUSA21lk9OVEEQxvsRDImoiMG9mLjjCG5mEg7gEfQGsIcF7p0EDsBBSJiNO7ZsFRZqosb/QkSj7fer7ur33sw8GDFUUq+7q6vqq6qu7pkQzqG4EeI521e7FePVgM9cGPYwhCi6UO8qFOK+YY+Br66ujsmmxb84Yzwp6zCsxjJfWVkxnMsEMGuWHZ9Wcz11cM48hkq0vLwc1tbW4mAwqDpcdIqnMmgF0JMv2CiGnZ2dcHR0FA4PD8Pe3t5U/tx6bCSlb+JT8XfxT3HsUek0Li0tRdjWl+z6iRF+FNA1hXPDQ/IMNyRg3s8bD/OaZS+VP+9cOLSa64cA34oXZWagDkRzAaJxXaE+ufc4rCN7LrazZ2+8+STtpAL8WYDvpTaHKlkB2iQARMvb2+H27m4YaL7zaDtUw1BZAASi6T8T2UZnPZV2pvnJfCH5p8bewcGB6TrIfz8wBZgHQ83kjpuj6RBYQpuo09Tvmpd7TPe+ktZN8cKwS92KWXGuaqWowlYEwthtMcWOZUNJc8at+zuF/Xkqo69baS7P+AvWjYwJ4jyHXXsEnd74ZO/Pq+uXUuv6WNlso6cvnDsZB1V/unJab3D1/KrJDw9NCM9wHf2FK2ejTKMejnBHfGtfH7LGGCdQDqaqJgfgzWjXK1nYV4jRbPGnxUT7cqUaZfJrVZeOm9QmB21L6xXgbu/ScsYusJFMoU0x2fsamRJOd6kOYDRLUxv94ENZe8+0gM+0dyz+KgU7X8rLHHCIOZyrna4y6ykIu0YCs02TBXmk3PZssmEgaTxTo83xjCIjoE21h0Yah3MrV4+9kR8MaabGze+9NEILGAFE5nMOiiA32KnAr/sb7tED3nzlzC4dB38WMC+EjaqHfqvUKHi2gJPdWQ6AbH8hgyQ7QY6jvjj3QZWvX6pUAtduTX5Dss96Q7NI9RQRJeeKvRFbt0v2gb1Gx/PooJsztn1c1DqpAU3Hde2dB2aEHBhjgOFjMeDvxLafjQ3YZQSgOcHJZX611H45sGLHWvYTz9hiURlpNoBZvxb/Ft9lAQ1DmBfUiR+j1hAPkMBTE9L9+zLva1QvGFHurRBaZ5xLVitoBviiRkD/sIMDztKA5FA0b9/0OclzO2/XAQymJ0TcghZwEo9/AX8gMeAJMOvIsWWt5bwCoiFhVSllrdH0t5Q1JHAFlKJNkvTVdn2GHb9KdmacMT+d/Os05imJUccRX2YuZ93Sxf0Ilc4DPDeAq5SAvFEAY94cQc6BA26dzb4HWAJI4DPmQE5KCVUyvb2FcDZem7JdT2ggKUP3xX6n9XNq1DpzSf4Cy4ZqSlmM8d8AAAAASUVORK5CYII=","DiscoveredBy":"Firebreather","Properties":[{"Name":"Flying","Power":1},{"Name":"Creature","Power":1},{"Name":"Explodey","Power":11}],"LastUsed":"2015-10-31 23:59:56 UTC"}],"TeaTime":"15:00:00","NextVacation":"2666-06-06","FavoriteTime":"2001-12-19T23:59:59","FavoriteNumber":"1.618033989"}
3+
{"Name":"Sabrina","Age":"17","Weight":128.3,"IsMagic":true,"Spells":[{"Name": "Talking cats", "Icon":"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAABxpRE9UAAAAAgAAAAAAAAAgAAAAKAAAACAAAAAgAAABxj2CfowAAAGSSURBVHgB7Jc9TsNAEIX3JDkCPUV6KlpKFHEGlD4nyA04ACUXQKTgCEipUnKGNEbP0otentayicZ24SlWs7tjO/N9u/5J2b2+NUtuZcnwYE8BuQPyGZAPwXwLLPk5kG+BJa9+fgfkh1B+CeancL4F8i2Q/wWm/S/w+XFoTseftn0dvhu0OXfhpM+AGvzcEiYVAFisPqE9zrETJhHAlXfg2lglMK9z0f3RBfB+ZyRUV3x+erzsEIjjOBqc1xtNAIrvguybV3A9lkVHxlEE6GrrPb/ZvAySwlUnfCmlPQ+R8JCExvGtcRQBLFwj4FGkznX1VYDKPG/f2/MjwCksXACgdNUxJjwK9xwl4JihOwTFR0kIF+CABEPRnvsvPFctMoYKqAFSAFaMwB4pp3Y+bodIYL9WmIAaIOHxo7W8wiHvAjTvhUeNwwSgeAeAABbqOewC5hBdwFD4+9+7puzXV9fS6/b1wwT4tsaYAhwOOQdUQch5vgZCeAhAv3ZM31yYAAUgvApQQQ6n5w6FB/RVe1jdJOAPAAD//1eMQwoAAAGQSURBVO1UMU4DQQy8X9AgWopIUINEkS4VlJQo4gvwAV7AD3gEH4iSgidESpWSXyyZExP5lr0c7K5PsXBhec/2+jzjuWtent9CLdtu1mG5+gjz+WNr7IsY7eH+tvO+xfuqk4vz7CH91edFaF5v9nb6dBKm13edvrL+0Lk5lMzJkQDeJSkkgHF6mR8CHwMHCQR/NAQQGD0BAlwK4FCefQiefq+A2Vn29tG7igLAfmwcnJu/nJy3BMQkMN9HEPr8AL3bfBv7Bp+7/SoExMDjZwKEJwmyhnnmQIQEBIlz2x0iKoAvJkAC6TsTIH6MqRrEWUMSZF2zAwqT4Eu/e6pzFAIkmNSZ4OFT+VYBIIF//UqbJwnF/4DU0GwOn8r/JQYCpPGufEfJuZiA37ycQw/5uFeqPq4pfR6FADmkBCXjfWdZj3NfXW58dAJyB9W65wRoMWulryvAyqa05nQFaDFrpa8rwMqmtOZ0BWgxa6WvK8DKprTmdAVoMWulryvAyqa05nQFaDFrpa8rwMqmtOb89wr4AtQ4aPoL6yVpAAAAAElFTkSuQmCC","DiscoveredBy":"Salem","Properties":[{"Name":"Makes you look crazy","Power":1}],"LastUsed":"2017-02-14 12:07:23 UTC"}],"TeaTime":"12:00:00","NextVacation":"2017-03-14","FavoriteTime":"2000-10-31T23:27:46","FavoriteNumber":"13"}

bigquery/tests/data/schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@
7878
"mode" : "NULLABLE",
7979
"name" : "FavoriteTime",
8080
"type" : "DATETIME"
81+
},
82+
{
83+
"mode" : "NULLABLE",
84+
"name" : "FavoriteNumber",
85+
"type" : "NUMERIC"
8186
}
8287
]
8388
}

bigquery/tests/system.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import concurrent.futures
1717
import csv
1818
import datetime
19+
import decimal
1920
import json
2021
import operator
2122
import os
@@ -959,6 +960,7 @@ def _generate_standard_sql_types_examples(self):
959960
stamp_microseconds = stamp + '.250000'
960961
zoned = naive.replace(tzinfo=UTC)
961962
zoned_microseconds = naive_microseconds.replace(tzinfo=UTC)
963+
numeric = decimal.Decimal('123456789.123456789')
962964
return [
963965
{
964966
'sql': 'SELECT 1',
@@ -1005,6 +1007,10 @@ def _generate_standard_sql_types_examples(self):
10051007
'sql': 'SELECT TIME(TIMESTAMP "%s")' % (stamp,),
10061008
'expected': naive.time(),
10071009
},
1010+
{
1011+
'sql': 'SELECT NUMERIC "%s"' % (numeric,),
1012+
'expected': numeric,
1013+
},
10081014
{
10091015
'sql': 'SELECT (1, 2)',
10101016
'expected': {'_field_1': 1, '_field_2': 2},
@@ -1257,6 +1263,10 @@ def test_query_w_query_params(self):
12571263
pi = 3.1415926
12581264
pi_param = ScalarQueryParameter(
12591265
name='pi', type_='FLOAT64', value=pi)
1266+
pi_numeric = decimal.Decimal('3.141592654')
1267+
pi_numeric_param = ScalarQueryParameter(
1268+
name='pi_numeric_param', type_='NUMERIC',
1269+
value=pi_numeric)
12601270
truthy = True
12611271
truthy_param = ScalarQueryParameter(
12621272
name='truthy', type_='BOOL', value=truthy)
@@ -1332,6 +1342,11 @@ def test_query_w_query_params(self):
13321342
'expected': pi,
13331343
'query_parameters': [pi_param],
13341344
},
1345+
{
1346+
'sql': 'SELECT @pi_numeric_param',
1347+
'expected': pi_numeric,
1348+
'query_parameters': [pi_numeric_param],
1349+
},
13351350
{
13361351
'sql': 'SELECT @truthy',
13371352
'expected': truthy,
@@ -1771,6 +1786,8 @@ def test_create_table_rows_fetch_nested_schema(self):
17711786
'%Y-%m-%dT%H:%M:%S')
17721787
e_favtime = datetime.datetime(*parts[0:6])
17731788
self.assertEqual(found[7], e_favtime)
1789+
self.assertEqual(found[8],
1790+
decimal.Decimal(expected['FavoriteNumber']))
17741791

17751792
def _fetch_dataframe(self, query):
17761793
return Config.CLIENT.query(query).result().to_dataframe()

bigquery/tests/unit/test__helpers.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import base64
1616
import datetime
17+
import decimal
1718
import unittest
1819

1920

@@ -80,6 +81,30 @@ def test_w_float_value(self):
8081
self.assertEqual(coerced, 3.1415)
8182

8283

84+
class Test_decimal_from_json(unittest.TestCase):
85+
86+
def _call_fut(self, value, field):
87+
from google.cloud.bigquery._helpers import _decimal_from_json
88+
89+
return _decimal_from_json(value, field)
90+
91+
def test_w_none_nullable(self):
92+
self.assertIsNone(self._call_fut(None, _Field('NULLABLE')))
93+
94+
def test_w_none_required(self):
95+
with self.assertRaises(TypeError):
96+
self._call_fut(None, _Field('REQUIRED'))
97+
98+
def test_w_string_value(self):
99+
coerced = self._call_fut('3.1415', object())
100+
self.assertEqual(coerced, decimal.Decimal('3.1415'))
101+
102+
def test_w_float_value(self):
103+
coerced = self._call_fut(3.1415, object())
104+
# There is no exact float representation of 3.1415.
105+
self.assertEqual(coerced, decimal.Decimal(3.1415))
106+
107+
83108
class Test_bool_from_json(unittest.TestCase):
84109

85110
def _call_fut(self, value, field):
@@ -585,6 +610,23 @@ def test_w_float(self):
585610
self.assertEqual(self._call_fut(1.23), 1.23)
586611

587612

613+
class Test_decimal_to_json(unittest.TestCase):
614+
615+
def _call_fut(self, value):
616+
from google.cloud.bigquery._helpers import _decimal_to_json
617+
618+
return _decimal_to_json(value)
619+
620+
def test_w_float(self):
621+
self.assertEqual(self._call_fut(1.23), 1.23)
622+
623+
def test_w_string(self):
624+
self.assertEqual(self._call_fut('1.23'), '1.23')
625+
626+
def test_w_decimal(self):
627+
self.assertEqual(self._call_fut(decimal.Decimal('1.23')), '1.23')
628+
629+
588630
class Test_bool_to_json(unittest.TestCase):
589631

590632
def _call_fut(self, value):

0 commit comments

Comments
 (0)