Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/cloud/bigquery/dbapi/_helpers.py: 26%
170 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:07 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:07 +0000
1# Copyright 2017 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
16from collections import abc as collections_abc
17import datetime
18import decimal
19import functools
20import numbers
21import re
22import typing
24from google.cloud import bigquery
25from google.cloud.bigquery import table, query
26from google.cloud.bigquery.dbapi import exceptions
29_NUMERIC_SERVER_MIN = decimal.Decimal("-9.9999999999999999999999999999999999999E+28")
30_NUMERIC_SERVER_MAX = decimal.Decimal("9.9999999999999999999999999999999999999E+28")
32type_parameters_re = re.compile(
33 r"""
34 \(
35 \s*[0-9]+\s*
36 (,
37 \s*[0-9]+\s*
38 )*
39 \)
40 """,
41 re.VERBOSE,
42)
45def _parameter_type(name, value, query_parameter_type=None, value_doc=""):
46 if query_parameter_type:
47 # Strip type parameters
48 query_parameter_type = type_parameters_re.sub("", query_parameter_type)
49 try:
50 parameter_type = getattr(
51 query.SqlParameterScalarTypes, query_parameter_type.upper()
52 )._type
53 except AttributeError:
54 raise exceptions.ProgrammingError(
55 f"The given parameter type, {query_parameter_type},"
56 f" for {name} is not a valid BigQuery scalar type."
57 )
58 else:
59 parameter_type = bigquery_scalar_type(value)
60 if parameter_type is None:
61 raise exceptions.ProgrammingError(
62 f"Encountered parameter {name} with "
63 f"{value_doc} value {value} of unexpected type."
64 )
65 return parameter_type
68def scalar_to_query_parameter(value, name=None, query_parameter_type=None):
69 """Convert a scalar value into a query parameter.
71 Args:
72 value (Any):
73 A scalar value to convert into a query parameter.
75 name (str):
76 (Optional) Name of the query parameter.
77 query_parameter_type (Optional[str]): Given type for the parameter.
79 Returns:
80 google.cloud.bigquery.ScalarQueryParameter:
81 A query parameter corresponding with the type and value of the plain
82 Python object.
84 Raises:
85 google.cloud.bigquery.dbapi.exceptions.ProgrammingError:
86 if the type cannot be determined.
87 """
88 return bigquery.ScalarQueryParameter(
89 name, _parameter_type(name, value, query_parameter_type), value
90 )
93def array_to_query_parameter(value, name=None, query_parameter_type=None):
94 """Convert an array-like value into a query parameter.
96 Args:
97 value (Sequence[Any]): The elements of the array (should not be a
98 string-like Sequence).
99 name (Optional[str]): Name of the query parameter.
100 query_parameter_type (Optional[str]): Given type for the parameter.
102 Returns:
103 A query parameter corresponding with the type and value of the plain
104 Python object.
106 Raises:
107 google.cloud.bigquery.dbapi.exceptions.ProgrammingError:
108 if the type of array elements cannot be determined.
109 """
110 if not array_like(value):
111 raise exceptions.ProgrammingError(
112 "The value of parameter {} must be a sequence that is "
113 "not string-like.".format(name)
114 )
116 if query_parameter_type or value:
117 array_type = _parameter_type(
118 name,
119 value[0] if value else None,
120 query_parameter_type,
121 value_doc="array element ",
122 )
123 else:
124 raise exceptions.ProgrammingError(
125 "Encountered an empty array-like value of parameter {}, cannot "
126 "determine array elements type.".format(name)
127 )
129 return bigquery.ArrayQueryParameter(name, array_type, value)
132def _parse_struct_fields(
133 fields,
134 base,
135 parse_struct_field=re.compile(
136 r"""
137 (?:(\w+)\s+) # field name
138 ([A-Z0-9<> ,()]+) # Field type
139 $""",
140 re.VERBOSE | re.IGNORECASE,
141 ).match,
142):
143 # Split a string of struct fields. They're defined by commas, but
144 # we have to avoid splitting on commas internal to fields. For
145 # example:
146 # name string, children array<struct<name string, bdate date>>
147 #
148 # only has 2 top-level fields.
149 fields = fields.split(",")
150 fields = list(reversed(fields)) # in the off chance that there are very many
151 while fields:
152 field = fields.pop()
153 while fields and field.count("<") != field.count(">"):
154 field += "," + fields.pop()
156 m = parse_struct_field(field.strip())
157 if not m:
158 raise exceptions.ProgrammingError(
159 f"Invalid struct field, {field}, in {base}"
160 )
161 yield m.group(1, 2)
164SCALAR, ARRAY, STRUCT = ("s", "a", "r")
167def _parse_type(
168 type_,
169 name,
170 base,
171 complex_query_parameter_parse=re.compile(
172 r"""
173 \s*
174 (ARRAY|STRUCT|RECORD) # Type
175 \s*
176 <([A-Z0-9_<> ,()]+)> # Subtype(s)
177 \s*$
178 """,
179 re.IGNORECASE | re.VERBOSE,
180 ).match,
181):
182 if "<" not in type_:
183 # Scalar
185 # Strip type parameters
186 type_ = type_parameters_re.sub("", type_).strip()
187 try:
188 type_ = getattr(query.SqlParameterScalarTypes, type_.upper())
189 except AttributeError:
190 raise exceptions.ProgrammingError(
191 f"The given parameter type, {type_},"
192 f"{' for ' + name if name else ''}"
193 f" is not a valid BigQuery scalar type, in {base}."
194 )
195 if name:
196 type_ = type_.with_name(name)
197 return SCALAR, type_
199 m = complex_query_parameter_parse(type_)
200 if not m:
201 raise exceptions.ProgrammingError(f"Invalid parameter type, {type_}")
202 tname, sub = m.group(1, 2)
203 if tname.upper() == "ARRAY":
204 sub_type = complex_query_parameter_type(None, sub, base)
205 if isinstance(sub_type, query.ArrayQueryParameterType):
206 raise exceptions.ProgrammingError(f"Array can't contain an array in {base}")
207 sub_type._complex__src = sub
208 return ARRAY, sub_type
209 else:
210 return STRUCT, _parse_struct_fields(sub, base)
213def complex_query_parameter_type(name: typing.Optional[str], type_: str, base: str):
214 """Construct a parameter type (`StructQueryParameterType`) for a complex type
216 or a non-complex type that's part of a complex type.
218 Examples:
220 array<struct<x float64, y float64>>
222 struct<name string, children array<struct<name string, bdate date>>>
224 This is used for computing array types.
225 """
227 type_type, sub_type = _parse_type(type_, name, base)
228 if type_type == SCALAR:
229 result_type = sub_type
230 elif type_type == ARRAY:
231 result_type = query.ArrayQueryParameterType(sub_type, name=name)
232 elif type_type == STRUCT:
233 fields = [
234 complex_query_parameter_type(field_name, field_type, base)
235 for field_name, field_type in sub_type
236 ]
237 result_type = query.StructQueryParameterType(*fields, name=name)
238 else: # pragma: NO COVER
239 raise AssertionError("Bad type_type", type_type) # Can't happen :)
241 return result_type
244def complex_query_parameter(
245 name: typing.Optional[str], value, type_: str, base: typing.Optional[str] = None
246):
247 """
248 Construct a query parameter for a complex type (array or struct record)
250 or for a subtype, which may not be complex
252 Examples:
254 array<struct<x float64, y float64>>
256 struct<name string, children array<struct<name string, bdate date>>>
258 """
259 param: typing.Union[
260 query.ScalarQueryParameter,
261 query.ArrayQueryParameter,
262 query.StructQueryParameter,
263 ]
265 base = base or type_
267 type_type, sub_type = _parse_type(type_, name, base)
269 if type_type == SCALAR:
270 param = query.ScalarQueryParameter(name, sub_type._type, value)
271 elif type_type == ARRAY:
272 if not array_like(value):
273 raise exceptions.ProgrammingError(
274 f"Array type with non-array-like value"
275 f" with type {type(value).__name__}"
276 )
277 param = query.ArrayQueryParameter(
278 name,
279 sub_type,
280 value
281 if isinstance(sub_type, query.ScalarQueryParameterType)
282 else [
283 complex_query_parameter(None, v, sub_type._complex__src, base)
284 for v in value
285 ],
286 )
287 elif type_type == STRUCT:
288 if not isinstance(value, collections_abc.Mapping):
289 raise exceptions.ProgrammingError(f"Non-mapping value for type {type_}")
290 value_keys = set(value)
291 fields = []
292 for field_name, field_type in sub_type:
293 if field_name not in value:
294 raise exceptions.ProgrammingError(
295 f"No field value for {field_name} in {type_}"
296 )
297 value_keys.remove(field_name)
298 fields.append(
299 complex_query_parameter(field_name, value[field_name], field_type, base)
300 )
301 if value_keys:
302 raise exceptions.ProgrammingError(f"Extra data keys for {type_}")
304 param = query.StructQueryParameter(name, *fields)
305 else: # pragma: NO COVER
306 raise AssertionError("Bad type_type", type_type) # Can't happen :)
308 return param
311def _dispatch_parameter(type_, value, name=None):
312 if type_ is not None and "<" in type_:
313 param = complex_query_parameter(name, value, type_)
314 elif isinstance(value, collections_abc.Mapping):
315 raise NotImplementedError(
316 f"STRUCT-like parameter values are not supported"
317 f"{' (parameter ' + name + ')' if name else ''},"
318 f" unless an explicit type is give in the parameter placeholder"
319 f" (e.g. '%({name if name else ''}:struct<...>)s')."
320 )
321 elif array_like(value):
322 param = array_to_query_parameter(value, name, type_)
323 else:
324 param = scalar_to_query_parameter(value, name, type_)
326 return param
329def to_query_parameters_list(parameters, parameter_types):
330 """Converts a sequence of parameter values into query parameters.
332 Args:
333 parameters (Sequence[Any]): Sequence of query parameter values.
334 parameter_types:
335 A list of parameter types, one for each parameter.
336 Unknown types are provided as None.
338 Returns:
339 List[google.cloud.bigquery.query._AbstractQueryParameter]:
340 A list of query parameters.
341 """
342 return [
343 _dispatch_parameter(type_, value)
344 for value, type_ in zip(parameters, parameter_types)
345 ]
348def to_query_parameters_dict(parameters, query_parameter_types):
349 """Converts a dictionary of parameter values into query parameters.
351 Args:
352 parameters (Mapping[str, Any]): Dictionary of query parameter values.
353 parameter_types:
354 A dictionary of parameter types. It needn't have a key for each
355 parameter.
357 Returns:
358 List[google.cloud.bigquery.query._AbstractQueryParameter]:
359 A list of named query parameters.
360 """
361 return [
362 _dispatch_parameter(query_parameter_types.get(name), value, name)
363 for name, value in parameters.items()
364 ]
367def to_query_parameters(parameters, parameter_types):
368 """Converts DB-API parameter values into query parameters.
370 Args:
371 parameters (Union[Mapping[str, Any], Sequence[Any]]):
372 A dictionary or sequence of query parameter values.
373 parameter_types (Union[Mapping[str, str], Sequence[str]]):
374 A dictionary or list of parameter types.
376 If parameters is a mapping, then this must be a dictionary
377 of parameter types. It needn't have a key for each
378 parameter.
380 If parameters is a sequence, then this must be a list of
381 parameter types, one for each paramater. Unknown types
382 are provided as None.
384 Returns:
385 List[google.cloud.bigquery.query._AbstractQueryParameter]:
386 A list of query parameters.
387 """
388 if parameters is None:
389 return []
391 if isinstance(parameters, collections_abc.Mapping):
392 return to_query_parameters_dict(parameters, parameter_types)
393 else:
394 return to_query_parameters_list(parameters, parameter_types)
397def bigquery_scalar_type(value):
398 """Return a BigQuery name of the scalar type that matches the given value.
400 If the scalar type name could not be determined (e.g. for non-scalar
401 values), ``None`` is returned.
403 Args:
404 value (Any)
406 Returns:
407 Optional[str]: The BigQuery scalar type name.
408 """
409 if isinstance(value, bool):
410 return "BOOL"
411 elif isinstance(value, numbers.Integral):
412 return "INT64"
413 elif isinstance(value, numbers.Real):
414 return "FLOAT64"
415 elif isinstance(value, decimal.Decimal):
416 vtuple = value.as_tuple()
417 # NUMERIC values have precision of 38 (number of digits) and scale of 9 (number
418 # of fractional digits), and their max absolute value must be strictly smaller
419 # than 1.0E+29.
420 # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#decimal_types
421 if (
422 len(vtuple.digits) <= 38 # max precision: 38
423 and vtuple.exponent >= -9 # max scale: 9
424 and _NUMERIC_SERVER_MIN <= value <= _NUMERIC_SERVER_MAX
425 ):
426 return "NUMERIC"
427 else:
428 return "BIGNUMERIC"
430 elif isinstance(value, str):
431 return "STRING"
432 elif isinstance(value, bytes):
433 return "BYTES"
434 elif isinstance(value, datetime.datetime):
435 return "DATETIME" if value.tzinfo is None else "TIMESTAMP"
436 elif isinstance(value, datetime.date):
437 return "DATE"
438 elif isinstance(value, datetime.time):
439 return "TIME"
441 return None
444def array_like(value):
445 """Determine if the given value is array-like.
447 Examples of array-like values (as interpreted by this function) are
448 sequences such as ``list`` and ``tuple``, but not strings and other
449 iterables such as sets.
451 Args:
452 value (Any)
454 Returns:
455 bool: ``True`` if the value is considered array-like, ``False`` otherwise.
456 """
457 return isinstance(value, collections_abc.Sequence) and not isinstance(
458 value, (str, bytes, bytearray)
459 )
462def to_bq_table_rows(rows_iterable):
463 """Convert table rows to BigQuery table Row instances.
465 Args:
466 rows_iterable (Iterable[Mapping]):
467 An iterable of row data items to convert to ``Row`` instances.
469 Returns:
470 Iterable[google.cloud.bigquery.table.Row]
471 """
473 def to_table_row(row):
474 # NOTE: We fetch ARROW values, thus we need to convert them to Python
475 # objects with as_py().
476 values = tuple(value.as_py() for value in row.values())
477 keys_to_index = {key: i for i, key in enumerate(row.keys())}
478 return table.Row(values, keys_to_index)
480 return (to_table_row(row_data) for row_data in rows_iterable)
483def raise_on_closed(
484 exc_msg, exc_class=exceptions.ProgrammingError, closed_attr_name="_closed"
485):
486 """Make public instance methods raise an error if the instance is closed."""
488 def _raise_on_closed(method):
489 """Make a non-static method raise an error if its containing instance is closed."""
491 def with_closed_check(self, *args, **kwargs):
492 if getattr(self, closed_attr_name):
493 raise exc_class(exc_msg)
494 return method(self, *args, **kwargs)
496 functools.update_wrapper(with_closed_check, method)
497 return with_closed_check
499 def decorate_public_methods(klass):
500 """Apply ``_raise_on_closed()`` decorator to public instance methods."""
501 for name in dir(klass):
502 if name.startswith("_") and name != "__iter__":
503 continue
505 member = getattr(klass, name)
506 if not callable(member):
507 continue
509 # We need to check for class/static methods directly in the instance
510 # __dict__, not via the retrieved attribute (`member`), as the
511 # latter is already a callable *produced* by one of these descriptors.
512 if isinstance(klass.__dict__[name], (staticmethod, classmethod)):
513 continue
515 member = _raise_on_closed(member)
516 setattr(klass, name, member)
518 return klass
520 return decorate_public_methods