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

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. 

14 

15 

16from collections import abc as collections_abc 

17import datetime 

18import decimal 

19import functools 

20import numbers 

21import re 

22import typing 

23 

24from google.cloud import bigquery 

25from google.cloud.bigquery import table, query 

26from google.cloud.bigquery.dbapi import exceptions 

27 

28 

29_NUMERIC_SERVER_MIN = decimal.Decimal("-9.9999999999999999999999999999999999999E+28") 

30_NUMERIC_SERVER_MAX = decimal.Decimal("9.9999999999999999999999999999999999999E+28") 

31 

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) 

43 

44 

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 

66 

67 

68def scalar_to_query_parameter(value, name=None, query_parameter_type=None): 

69 """Convert a scalar value into a query parameter. 

70 

71 Args: 

72 value (Any): 

73 A scalar value to convert into a query parameter. 

74 

75 name (str): 

76 (Optional) Name of the query parameter. 

77 query_parameter_type (Optional[str]): Given type for the parameter. 

78 

79 Returns: 

80 google.cloud.bigquery.ScalarQueryParameter: 

81 A query parameter corresponding with the type and value of the plain 

82 Python object. 

83 

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 ) 

91 

92 

93def array_to_query_parameter(value, name=None, query_parameter_type=None): 

94 """Convert an array-like value into a query parameter. 

95 

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. 

101 

102 Returns: 

103 A query parameter corresponding with the type and value of the plain 

104 Python object. 

105 

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 ) 

115 

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 ) 

128 

129 return bigquery.ArrayQueryParameter(name, array_type, value) 

130 

131 

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() 

155 

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) 

162 

163 

164SCALAR, ARRAY, STRUCT = ("s", "a", "r") 

165 

166 

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 

184 

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_ 

198 

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) 

211 

212 

213def complex_query_parameter_type(name: typing.Optional[str], type_: str, base: str): 

214 """Construct a parameter type (`StructQueryParameterType`) for a complex type 

215 

216 or a non-complex type that's part of a complex type. 

217 

218 Examples: 

219 

220 array<struct<x float64, y float64>> 

221 

222 struct<name string, children array<struct<name string, bdate date>>> 

223 

224 This is used for computing array types. 

225 """ 

226 

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 :) 

240 

241 return result_type 

242 

243 

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) 

249 

250 or for a subtype, which may not be complex 

251 

252 Examples: 

253 

254 array<struct<x float64, y float64>> 

255 

256 struct<name string, children array<struct<name string, bdate date>>> 

257 

258 """ 

259 param: typing.Union[ 

260 query.ScalarQueryParameter, 

261 query.ArrayQueryParameter, 

262 query.StructQueryParameter, 

263 ] 

264 

265 base = base or type_ 

266 

267 type_type, sub_type = _parse_type(type_, name, base) 

268 

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_}") 

303 

304 param = query.StructQueryParameter(name, *fields) 

305 else: # pragma: NO COVER 

306 raise AssertionError("Bad type_type", type_type) # Can't happen :) 

307 

308 return param 

309 

310 

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_) 

325 

326 return param 

327 

328 

329def to_query_parameters_list(parameters, parameter_types): 

330 """Converts a sequence of parameter values into query parameters. 

331 

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. 

337 

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 ] 

346 

347 

348def to_query_parameters_dict(parameters, query_parameter_types): 

349 """Converts a dictionary of parameter values into query parameters. 

350 

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. 

356 

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 ] 

365 

366 

367def to_query_parameters(parameters, parameter_types): 

368 """Converts DB-API parameter values into query parameters. 

369 

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. 

375 

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. 

379 

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. 

383 

384 Returns: 

385 List[google.cloud.bigquery.query._AbstractQueryParameter]: 

386 A list of query parameters. 

387 """ 

388 if parameters is None: 

389 return [] 

390 

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) 

395 

396 

397def bigquery_scalar_type(value): 

398 """Return a BigQuery name of the scalar type that matches the given value. 

399 

400 If the scalar type name could not be determined (e.g. for non-scalar 

401 values), ``None`` is returned. 

402 

403 Args: 

404 value (Any) 

405 

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" 

429 

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" 

440 

441 return None 

442 

443 

444def array_like(value): 

445 """Determine if the given value is array-like. 

446 

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. 

450 

451 Args: 

452 value (Any) 

453 

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 ) 

460 

461 

462def to_bq_table_rows(rows_iterable): 

463 """Convert table rows to BigQuery table Row instances. 

464 

465 Args: 

466 rows_iterable (Iterable[Mapping]): 

467 An iterable of row data items to convert to ``Row`` instances. 

468 

469 Returns: 

470 Iterable[google.cloud.bigquery.table.Row] 

471 """ 

472 

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) 

479 

480 return (to_table_row(row_data) for row_data in rows_iterable) 

481 

482 

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.""" 

487 

488 def _raise_on_closed(method): 

489 """Make a non-static method raise an error if its containing instance is closed.""" 

490 

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) 

495 

496 functools.update_wrapper(with_closed_check, method) 

497 return with_closed_check 

498 

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 

504 

505 member = getattr(klass, name) 

506 if not callable(member): 

507 continue 

508 

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 

514 

515 member = _raise_on_closed(member) 

516 setattr(klass, name, member) 

517 

518 return klass 

519 

520 return decorate_public_methods