1818import decimal
1919import functools
2020import numbers
21+ import re
22+ import typing
2123
2224from google .cloud import bigquery
23- from google .cloud .bigquery import table , enums
25+ from google .cloud .bigquery import table , enums , query
2426from google .cloud .bigquery .dbapi import exceptions
2527
2628
2729_NUMERIC_SERVER_MIN = decimal .Decimal ("-9.9999999999999999999999999999999999999E+28" )
2830_NUMERIC_SERVER_MAX = decimal .Decimal ("9.9999999999999999999999999999999999999E+28" )
2931
32+ type_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+
3044
3145def _parameter_type (name , value , query_parameter_type = None , value_doc = "" ):
3246 if query_parameter_type :
47+ # Strip type parameters
48+ query_parameter_type = type_parameters_re .sub ("" , query_parameter_type )
3349 try :
3450 parameter_type = getattr (
3551 enums .SqlParameterScalarTypes , query_parameter_type .upper ()
@@ -113,6 +129,197 @@ def array_to_query_parameter(value, name=None, query_parameter_type=None):
113129 return bigquery .ArrayQueryParameter (name , array_type , value )
114130
115131
132+ def _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+
164+ SCALAR , ARRAY , STRUCT = "sar"
165+
166+
167+ def _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 (enums .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+
213+ def 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+ type_ = sub_type
230+ elif type_type == ARRAY :
231+ 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+ 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 type_
242+
243+
244+ def 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+ base = base or type_
260+
261+ type_type , sub_type = _parse_type (type_ , name , base )
262+
263+ if type_type == SCALAR :
264+ param = query .ScalarQueryParameter (name , sub_type ._type , value )
265+ elif type_type == ARRAY :
266+ if not array_like (value ):
267+ raise exceptions .ProgrammingError (
268+ f"Array type with non-array-like value"
269+ f" with type { type (value ).__name__ } "
270+ )
271+ param = query .ArrayQueryParameter (
272+ name ,
273+ sub_type ,
274+ value
275+ if isinstance (sub_type , query .ScalarQueryParameterType )
276+ else [
277+ complex_query_parameter (None , v , sub_type ._complex__src , base )
278+ for v in value
279+ ],
280+ )
281+ elif type_type == STRUCT :
282+ if not isinstance (value , collections_abc .Mapping ):
283+ raise exceptions .ProgrammingError (f"Non-mapping value for type { type_ } " )
284+ value_keys = set (value )
285+ fields = []
286+ for field_name , field_type in sub_type :
287+ if field_name not in value :
288+ raise exceptions .ProgrammingError (
289+ f"No field value for { field_name } in { type_ } "
290+ )
291+ value_keys .remove (field_name )
292+ fields .append (
293+ complex_query_parameter (field_name , value [field_name ], field_type , base )
294+ )
295+ if value_keys :
296+ raise exceptions .ProgrammingError (f"Extra data keys for { type_ } " )
297+
298+ param = query .StructQueryParameter (name , * fields )
299+ else : # pragma: NO COVER
300+ raise AssertionError ("Bad type_type" , type_type ) # Can't happen :)
301+
302+ return param
303+
304+
305+ def _dispatch_parameter (type_ , value , name = None ):
306+ if type_ is not None and "<" in type_ :
307+ param = complex_query_parameter (name , value , type_ )
308+ elif isinstance (value , collections_abc .Mapping ):
309+ raise NotImplementedError (
310+ f"STRUCT-like parameter values are not supported"
311+ f"{ ' (parameter ' + name + ')' if name else '' } ,"
312+ f" unless an explicit type is give in the parameter placeholder"
313+ f" (e.g. '%({ name if name else '' } :struct<...>)s')."
314+ )
315+ elif array_like (value ):
316+ param = array_to_query_parameter (value , name , type_ )
317+ else :
318+ param = scalar_to_query_parameter (value , name , type_ )
319+
320+ return param
321+
322+
116323def to_query_parameters_list (parameters , parameter_types ):
117324 """Converts a sequence of parameter values into query parameters.
118325
@@ -126,19 +333,10 @@ def to_query_parameters_list(parameters, parameter_types):
126333 List[google.cloud.bigquery.query._AbstractQueryParameter]:
127334 A list of query parameters.
128335 """
129- result = []
130-
131- for value , type_ in zip (parameters , parameter_types ):
132- if isinstance (value , collections_abc .Mapping ):
133- raise NotImplementedError ("STRUCT-like parameter values are not supported." )
134- elif array_like (value ):
135- param = array_to_query_parameter (value , None , type_ )
136- else :
137- param = scalar_to_query_parameter (value , None , type_ )
138-
139- result .append (param )
140-
141- return result
336+ return [
337+ _dispatch_parameter (type_ , value )
338+ for value , type_ in zip (parameters , parameter_types )
339+ ]
142340
143341
144342def to_query_parameters_dict (parameters , query_parameter_types ):
@@ -154,28 +352,10 @@ def to_query_parameters_dict(parameters, query_parameter_types):
154352 List[google.cloud.bigquery.query._AbstractQueryParameter]:
155353 A list of named query parameters.
156354 """
157- result = []
158-
159- for name , value in parameters .items ():
160- if isinstance (value , collections_abc .Mapping ):
161- raise NotImplementedError (
162- "STRUCT-like parameter values are not supported "
163- "(parameter {})." .format (name )
164- )
165- else :
166- query_parameter_type = query_parameter_types .get (name )
167- if array_like (value ):
168- param = array_to_query_parameter (
169- value , name = name , query_parameter_type = query_parameter_type
170- )
171- else :
172- param = scalar_to_query_parameter (
173- value , name = name , query_parameter_type = query_parameter_type ,
174- )
175-
176- result .append (param )
177-
178- return result
355+ return [
356+ _dispatch_parameter (query_parameter_types .get (name ), value , name )
357+ for name , value in parameters .items ()
358+ ]
179359
180360
181361def to_query_parameters (parameters , parameter_types ):
0 commit comments