Content-Length: 875726 | pFad | http://github.com/MaxxleLLC/google-cloud-python/commit/ca57b99d82364dcfe3ff64dc0b932219519aa48c

EA BigQuery: Add support for array parameters to Cursor.execute() (#9189) · MaxxleLLC/google-cloud-python@ca57b99 · GitHub
Skip to content

Commit ca57b99

Browse files
plamutemar-kar
authored andcommitted
BigQuery: Add support for array parameters to Cursor.execute() (googleapis#9189)
* Add support for array params to Cursor.execute() * Raise NotImplementedError for STRUCT-like values
1 parent 7f33f62 commit ca57b99

File tree

2 files changed

+214
-25
lines changed

2 files changed

+214
-25
lines changed

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

Lines changed: 121 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,9 @@ def scalar_to_query_parameter(value, name=None):
4343
:raises: :class:`~google.cloud.bigquery.dbapi.exceptions.ProgrammingError`
4444
if the type cannot be determined.
4545
"""
46-
parameter_type = None
46+
parameter_type = bigquery_scalar_type(value)
4747

48-
if isinstance(value, bool):
49-
parameter_type = "BOOL"
50-
elif isinstance(value, numbers.Integral):
51-
parameter_type = "INT64"
52-
elif isinstance(value, numbers.Real):
53-
parameter_type = "FLOAT64"
54-
elif isinstance(value, decimal.Decimal):
55-
parameter_type = "NUMERIC"
56-
elif isinstance(value, six.text_type):
57-
parameter_type = "STRING"
58-
elif isinstance(value, six.binary_type):
59-
parameter_type = "BYTES"
60-
elif isinstance(value, datetime.datetime):
61-
parameter_type = "DATETIME" if value.tzinfo is None else "TIMESTAMP"
62-
elif isinstance(value, datetime.date):
63-
parameter_type = "DATE"
64-
elif isinstance(value, datetime.time):
65-
parameter_type = "TIME"
66-
else:
48+
if parameter_type is None:
6749
raise exceptions.ProgrammingError(
6850
"encountered parameter {} with value {} of unexpected type".format(
6951
name, value
@@ -72,6 +54,46 @@ def scalar_to_query_parameter(value, name=None):
7254
return bigquery.ScalarQueryParameter(name, parameter_type, value)
7355

7456

57+
def array_to_query_parameter(value, name=None):
58+
"""Convert an array-like value into a query parameter.
59+
60+
Args:
61+
value (Sequence[Any]): The elements of the array (should not be a
62+
string-like Sequence).
63+
name (Optional[str]): Name of the query parameter.
64+
65+
Returns:
66+
A query parameter corresponding with the type and value of the plain
67+
Python object.
68+
69+
Raises:
70+
:class:`~google.cloud.bigquery.dbapi.exceptions.ProgrammingError`
71+
if the type of array elements cannot be determined.
72+
"""
73+
if not array_like(value):
74+
raise exceptions.ProgrammingError(
75+
"The value of parameter {} must be a sequence that is "
76+
"not string-like.".format(name)
77+
)
78+
79+
if not value:
80+
raise exceptions.ProgrammingError(
81+
"Encountered an empty array-like value of parameter {}, cannot "
82+
"determine array elements type.".format(name)
83+
)
84+
85+
# Assume that all elements are of the same type, and let the backend handle
86+
# any type incompatibilities among the array elements
87+
array_type = bigquery_scalar_type(value[0])
88+
if array_type is None:
89+
raise exceptions.ProgrammingError(
90+
"Encountered unexpected first array element of parameter {}, "
91+
"cannot determine array elements type.".format(name)
92+
)
93+
94+
return bigquery.ArrayQueryParameter(name, array_type, value)
95+
96+
7597
def to_query_parameters_list(parameters):
7698
"""Converts a sequence of parameter values into query parameters.
7799
@@ -81,7 +103,18 @@ def to_query_parameters_list(parameters):
81103
:rtype: List[google.cloud.bigquery.query._AbstractQueryParameter]
82104
:returns: A list of query parameters.
83105
"""
84-
return [scalar_to_query_parameter(value) for value in parameters]
106+
result = []
107+
108+
for value in parameters:
109+
if isinstance(value, collections_abc.Mapping):
110+
raise NotImplementedError("STRUCT-like parameter values are not supported.")
111+
elif array_like(value):
112+
param = array_to_query_parameter(value)
113+
else:
114+
param = scalar_to_query_parameter(value)
115+
result.append(param)
116+
117+
return result
85118

86119

87120
def to_query_parameters_dict(parameters):
@@ -93,10 +126,21 @@ def to_query_parameters_dict(parameters):
93126
:rtype: List[google.cloud.bigquery.query._AbstractQueryParameter]
94127
:returns: A list of named query parameters.
95128
"""
96-
return [
97-
scalar_to_query_parameter(value, name=name)
98-
for name, value in six.iteritems(parameters)
99-
]
129+
result = []
130+
131+
for name, value in six.iteritems(parameters):
132+
if isinstance(value, collections_abc.Mapping):
133+
raise NotImplementedError(
134+
"STRUCT-like parameter values are not supported "
135+
"(parameter {}).".format(name)
136+
)
137+
elif array_like(value):
138+
param = array_to_query_parameter(value, name=name)
139+
else:
140+
param = scalar_to_query_parameter(value, name=name)
141+
result.append(param)
142+
143+
return result
100144

101145

102146
def to_query_parameters(parameters):
@@ -115,3 +159,55 @@ def to_query_parameters(parameters):
115159
return to_query_parameters_dict(parameters)
116160

117161
return to_query_parameters_list(parameters)
162+
163+
164+
def bigquery_scalar_type(value):
165+
"""Return a BigQuery name of the scalar type that matches the given value.
166+
167+
If the scalar type name could not be determined (e.g. for non-scalar
168+
values), ``None`` is returned.
169+
170+
Args:
171+
value (Any)
172+
173+
Returns:
174+
Optional[str]: The BigQuery scalar type name.
175+
"""
176+
if isinstance(value, bool):
177+
return "BOOL"
178+
elif isinstance(value, numbers.Integral):
179+
return "INT64"
180+
elif isinstance(value, numbers.Real):
181+
return "FLOAT64"
182+
elif isinstance(value, decimal.Decimal):
183+
return "NUMERIC"
184+
elif isinstance(value, six.text_type):
185+
return "STRING"
186+
elif isinstance(value, six.binary_type):
187+
return "BYTES"
188+
elif isinstance(value, datetime.datetime):
189+
return "DATETIME" if value.tzinfo is None else "TIMESTAMP"
190+
elif isinstance(value, datetime.date):
191+
return "DATE"
192+
elif isinstance(value, datetime.time):
193+
return "TIME"
194+
195+
return None
196+
197+
198+
def array_like(value):
199+
"""Determine if the given value is array-like.
200+
201+
Examples of array-like values (as interpreted by this function) are
202+
sequences such as ``list`` and ``tuple``, but not strings and other
203+
iterables such as sets.
204+
205+
Args:
206+
value (Any)
207+
208+
Returns:
209+
bool: ``True`` if the value is considered array-like, ``False`` otherwise.
210+
"""
211+
return isinstance(value, collections_abc.Sequence) and not isinstance(
212+
value, (six.text_type, six.binary_type, bytearray)
213+
)

bigquery/tests/unit/test_dbapi__helpers.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,61 @@ def test_scalar_to_query_parameter_w_special_floats(self):
6666
self.assertTrue(math.isinf(inf_parameter.value))
6767
self.assertEqual(inf_parameter.type_, "FLOAT64")
6868

69+
def test_array_to_query_parameter_valid_argument(self):
70+
expected_types = [
71+
([True, False], "BOOL"),
72+
([123, -456, 0], "INT64"),
73+
([1.25, 2.50], "FLOAT64"),
74+
([decimal.Decimal("1.25")], "NUMERIC"),
75+
([b"foo", b"bar"], "BYTES"),
76+
([u"foo", u"bar"], "STRING"),
77+
([datetime.date(2017, 4, 1), datetime.date(2018, 4, 1)], "DATE"),
78+
([datetime.time(12, 34, 56), datetime.time(10, 20, 30)], "TIME"),
79+
(
80+
[
81+
datetime.datetime(2012, 3, 4, 5, 6, 7),
82+
datetime.datetime(2013, 1, 1, 10, 20, 30),
83+
],
84+
"DATETIME",
85+
),
86+
(
87+
[
88+
datetime.datetime(
89+
2012, 3, 4, 5, 6, 7, tzinfo=google.cloud._helpers.UTC
90+
),
91+
datetime.datetime(
92+
2013, 1, 1, 10, 20, 30, tzinfo=google.cloud._helpers.UTC
93+
),
94+
],
95+
"TIMESTAMP",
96+
),
97+
]
98+
99+
for values, expected_type in expected_types:
100+
msg = "value: {} expected_type: {}".format(values, expected_type)
101+
parameter = _helpers.array_to_query_parameter(values)
102+
self.assertIsNone(parameter.name, msg=msg)
103+
self.assertEqual(parameter.array_type, expected_type, msg=msg)
104+
self.assertEqual(parameter.values, values, msg=msg)
105+
named_param = _helpers.array_to_query_parameter(values, name="my_param")
106+
self.assertEqual(named_param.name, "my_param", msg=msg)
107+
self.assertEqual(named_param.array_type, expected_type, msg=msg)
108+
self.assertEqual(named_param.values, values, msg=msg)
109+
110+
def test_array_to_query_parameter_empty_argument(self):
111+
with self.assertRaises(exceptions.ProgrammingError):
112+
_helpers.array_to_query_parameter([])
113+
114+
def test_array_to_query_parameter_unsupported_sequence(self):
115+
unsupported_iterables = [{10, 20, 30}, u"foo", b"bar", bytearray([65, 75, 85])]
116+
for iterable in unsupported_iterables:
117+
with self.assertRaises(exceptions.ProgrammingError):
118+
_helpers.array_to_query_parameter(iterable)
119+
120+
def test_array_to_query_parameter_sequence_w_invalid_elements(self):
121+
with self.assertRaises(exceptions.ProgrammingError):
122+
_helpers.array_to_query_parameter([object(), 2, 7])
123+
69124
def test_to_query_parameters_w_dict(self):
70125
parameters = {"somebool": True, "somestring": u"a-string-value"}
71126
query_parameters = _helpers.to_query_parameters(parameters)
@@ -82,6 +137,23 @@ def test_to_query_parameters_w_dict(self):
82137
),
83138
)
84139

140+
def test_to_query_parameters_w_dict_array_param(self):
141+
parameters = {"somelist": [10, 20]}
142+
query_parameters = _helpers.to_query_parameters(parameters)
143+
144+
self.assertEqual(len(query_parameters), 1)
145+
param = query_parameters[0]
146+
147+
self.assertEqual(param.name, "somelist")
148+
self.assertEqual(param.array_type, "INT64")
149+
self.assertEqual(param.values, [10, 20])
150+
151+
def test_to_query_parameters_w_dict_dict_param(self):
152+
parameters = {"my_param": {"foo": "bar"}}
153+
154+
with self.assertRaises(NotImplementedError):
155+
_helpers.to_query_parameters(parameters)
156+
85157
def test_to_query_parameters_w_list(self):
86158
parameters = [True, u"a-string-value"]
87159
query_parameters = _helpers.to_query_parameters(parameters)
@@ -92,3 +164,24 @@ def test_to_query_parameters_w_list(self):
92164
sorted(query_parameter_tuples),
93165
sorted([(None, "BOOL", True), (None, "STRING", u"a-string-value")]),
94166
)
167+
168+
def test_to_query_parameters_w_list_array_param(self):
169+
parameters = [[10, 20]]
170+
query_parameters = _helpers.to_query_parameters(parameters)
171+
172+
self.assertEqual(len(query_parameters), 1)
173+
param = query_parameters[0]
174+
175+
self.assertIsNone(param.name)
176+
self.assertEqual(param.array_type, "INT64")
177+
self.assertEqual(param.values, [10, 20])
178+
179+
def test_to_query_parameters_w_list_dict_param(self):
180+
parameters = [{"foo": "bar"}]
181+
182+
with self.assertRaises(NotImplementedError):
183+
_helpers.to_query_parameters(parameters)
184+
185+
def test_to_query_parameters_none_argument(self):
186+
query_parameters = _helpers.to_query_parameters(None)
187+
self.assertEqual(query_parameters, [])

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/MaxxleLLC/google-cloud-python/commit/ca57b99d82364dcfe3ff64dc0b932219519aa48c

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy