Skip to content

Commit 75c4115

Browse files
authored
Add support for query parameters (googleapis#2776)
* Add 'ScalarQueryParameter' class. Holds name, type, and value for scalar query parameters, and handles marshalling them to / from JSON representation mandated by the BigQuery API. * Factor out 'AbstractQueryParameter. * Add 'ArrayQueryParameter' class. Holds name, type, and value for array query parameters, and handles marshalling them to / from JSON representation mandated by the BigQuery API. * Add 'StructQueryParameter' class. Holds name, types, and values for Struct query parameters, and handles marshalling them to / from JSON representation mandated by the BigQuery API. * Add 'QueryParametersProperty' descriptor class. * Add 'query_parameters' property to 'QueryResults' and 'QueryJob'. * Plumb 'udf_resources'/'query_parameters' through client query factories. * Expose concrete query parameter classes as package APIs. Closes googleapis#2551.
1 parent e4ce734 commit 75c4115

File tree

11 files changed

+1158
-98
lines changed

11 files changed

+1158
-98
lines changed

bigquery/google/cloud/bigquery/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
"""
2424

2525

26+
from google.cloud.bigquery._helpers import ArrayQueryParameter
27+
from google.cloud.bigquery._helpers import ScalarQueryParameter
28+
from google.cloud.bigquery._helpers import StructQueryParameter
2629
from google.cloud.bigquery.client import Client
2730
from google.cloud.bigquery.dataset import AccessGrant
2831
from google.cloud.bigquery.dataset import Dataset

bigquery/google/cloud/bigquery/_helpers.py

Lines changed: 275 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
"""Shared helper functions for BigQuery API classes."""
1616

17+
from collections import OrderedDict
18+
1719
from google.cloud._helpers import _datetime_from_microseconds
1820
from google.cloud._helpers import _date_from_iso8601_date
1921

@@ -230,16 +232,279 @@ def __set__(self, instance, value):
230232
instance._udf_resources = tuple(value)
231233

232234

233-
def _build_udf_resources(resources):
235+
class AbstractQueryParameter(object):
236+
"""Base class for named / positional query parameters.
234237
"""
235-
:type resources: sequence of :class:`UDFResource`
236-
:param resources: fields to be appended.
238+
@classmethod
239+
def from_api_repr(cls, resource):
240+
"""Factory: construct paramter from JSON resource.
241+
242+
:type resource: dict
243+
:param resource: JSON mapping of parameter
244+
245+
:rtype: :class:`ScalarQueryParameter`
246+
"""
247+
raise NotImplementedError
248+
249+
def to_api_repr(self):
250+
"""Construct JSON API representation for the parameter.
251+
252+
:rtype: dict
253+
"""
254+
raise NotImplementedError
255+
237256

238-
:rtype: mapping
239-
:returns: a mapping describing userDefinedFunctionResources for the query.
257+
class ScalarQueryParameter(AbstractQueryParameter):
258+
"""Named / positional query parameters for scalar values.
259+
260+
:type name: str or None
261+
:param name: Parameter name, used via ``@foo`` syntax. If None, the
262+
paramter can only be addressed via position (``?``).
263+
264+
:type type_: str
265+
:param type_: name of parameter type. One of `'STRING'`, `'INT64'`,
266+
`'FLOAT64'`, `'BOOL'`, `'TIMESTAMP'`, or `'DATE'`.
267+
268+
:type value: str, int, float, bool, :class:`datetime.datetime`, or
269+
:class:`datetime.date`.
270+
:param value: the scalar parameter value.
240271
"""
241-
udfs = []
242-
for resource in resources:
243-
udf = {resource.udf_type: resource.value}
244-
udfs.append(udf)
245-
return udfs
272+
def __init__(self, name, type_, value):
273+
self.name = name
274+
self.type_ = type_
275+
self.value = value
276+
277+
@classmethod
278+
def positional(cls, type_, value):
279+
"""Factory for positional paramters.
280+
281+
:type type_: str
282+
:param type_: name of paramter type. One of `'STRING'`, `'INT64'`,
283+
`'FLOAT64'`, `'BOOL'`, `'TIMESTAMP'`, or `'DATE'`.
284+
285+
:type value: str, int, float, bool, :class:`datetime.datetime`, or
286+
:class:`datetime.date`.
287+
:param value: the scalar parameter value.
288+
289+
:rtype: :class:`ScalarQueryParameter`
290+
:returns: instance without name
291+
"""
292+
return cls(None, type_, value)
293+
294+
@classmethod
295+
def from_api_repr(cls, resource):
296+
"""Factory: construct paramter from JSON resource.
297+
298+
:type resource: dict
299+
:param resource: JSON mapping of parameter
300+
301+
:rtype: :class:`ScalarQueryParameter`
302+
:returns: instance
303+
"""
304+
name = resource.get('name')
305+
type_ = resource['parameterType']['type']
306+
value = resource['parameterValue']['value']
307+
converted = _CELLDATA_FROM_JSON[type_](value, None)
308+
return cls(name, type_, converted)
309+
310+
def to_api_repr(self):
311+
"""Construct JSON API representation for the parameter.
312+
313+
:rtype: dict
314+
:returns: JSON mapping
315+
"""
316+
resource = {
317+
'parameterType': {
318+
'type': self.type_,
319+
},
320+
'parameterValue': {
321+
'value': self.value,
322+
},
323+
}
324+
if self.name is not None:
325+
resource['name'] = self.name
326+
return resource
327+
328+
329+
class ArrayQueryParameter(AbstractQueryParameter):
330+
"""Named / positional query parameters for array values.
331+
332+
:type name: str or None
333+
:param name: Parameter name, used via ``@foo`` syntax. If None, the
334+
paramter can only be addressed via position (``?``).
335+
336+
:type array_type: str
337+
:param array_type:
338+
name of type of array elements. One of `'STRING'`, `'INT64'`,
339+
`'FLOAT64'`, `'BOOL'`, `'TIMESTAMP'`, or `'DATE'`.
340+
341+
:type values: list of appropriate scalar type.
342+
:param values: the parameter array values.
343+
"""
344+
def __init__(self, name, array_type, values):
345+
self.name = name
346+
self.array_type = array_type
347+
self.values = values
348+
349+
@classmethod
350+
def positional(cls, array_type, values):
351+
"""Factory for positional paramters.
352+
353+
:type array_type: str
354+
:param array_type:
355+
name of type of array elements. One of `'STRING'`, `'INT64'`,
356+
`'FLOAT64'`, `'BOOL'`, `'TIMESTAMP'`, or `'DATE'`.
357+
358+
:type values: list of appropriate scalar type
359+
:param values: the parameter array values.
360+
361+
:rtype: :class:`ArrayQueryParameter`
362+
:returns: instance without name
363+
"""
364+
return cls(None, array_type, values)
365+
366+
@classmethod
367+
def from_api_repr(cls, resource):
368+
"""Factory: construct paramter from JSON resource.
369+
370+
:type resource: dict
371+
:param resource: JSON mapping of parameter
372+
373+
:rtype: :class:`ArrayQueryParameter`
374+
:returns: instance
375+
"""
376+
name = resource.get('name')
377+
array_type = resource['parameterType']['arrayType']
378+
values = resource['parameterValue']['arrayValues']
379+
converted = [
380+
_CELLDATA_FROM_JSON[array_type](value, None) for value in values]
381+
return cls(name, array_type, converted)
382+
383+
def to_api_repr(self):
384+
"""Construct JSON API representation for the parameter.
385+
386+
:rtype: dict
387+
:returns: JSON mapping
388+
"""
389+
resource = {
390+
'parameterType': {
391+
'arrayType': self.array_type,
392+
},
393+
'parameterValue': {
394+
'arrayValues': self.values,
395+
},
396+
}
397+
if self.name is not None:
398+
resource['name'] = self.name
399+
return resource
400+
401+
402+
class StructQueryParameter(AbstractQueryParameter):
403+
"""Named / positional query parameters for struct values.
404+
405+
:type name: str or None
406+
:param name: Parameter name, used via ``@foo`` syntax. If None, the
407+
paramter can only be addressed via position (``?``).
408+
409+
:type sub_params: tuple of :class:`ScalarQueryParameter`
410+
:param sub_params: the sub-parameters for the struct
411+
"""
412+
def __init__(self, name, *sub_params):
413+
self.name = name
414+
self.struct_types = OrderedDict(
415+
(sub.name, sub.type_) for sub in sub_params)
416+
self.struct_values = {sub.name: sub.value for sub in sub_params}
417+
418+
@classmethod
419+
def positional(cls, *sub_params):
420+
"""Factory for positional paramters.
421+
422+
:type sub_params: tuple of :class:`ScalarQueryParameter`
423+
:param sub_params: the sub-parameters for the struct
424+
425+
:rtype: :class:`StructQueryParameter`
426+
:returns: instance without name
427+
"""
428+
return cls(None, *sub_params)
429+
430+
@classmethod
431+
def from_api_repr(cls, resource):
432+
"""Factory: construct paramter from JSON resource.
433+
434+
:type resource: dict
435+
:param resource: JSON mapping of parameter
436+
437+
:rtype: :class:`StructQueryParameter`
438+
:returns: instance
439+
"""
440+
name = resource.get('name')
441+
instance = cls(name)
442+
types = instance.struct_types
443+
for item in resource['parameterType']['structTypes']:
444+
types[item['name']] = item['type']
445+
struct_values = resource['parameterValue']['structValues']
446+
for key, value in struct_values.items():
447+
converted = _CELLDATA_FROM_JSON[types[key]](value, None)
448+
instance.struct_values[key] = converted
449+
return instance
450+
451+
def to_api_repr(self):
452+
"""Construct JSON API representation for the parameter.
453+
454+
:rtype: dict
455+
:returns: JSON mapping
456+
"""
457+
types = [
458+
{'name': key, 'type': value}
459+
for key, value in self.struct_types.items()
460+
]
461+
resource = {
462+
'parameterType': {
463+
'structTypes': types,
464+
},
465+
'parameterValue': {
466+
'structValues': self.struct_values,
467+
},
468+
}
469+
if self.name is not None:
470+
resource['name'] = self.name
471+
return resource
472+
473+
474+
class QueryParametersProperty(object):
475+
"""Custom property type, holding query parameter instances."""
476+
477+
def __get__(self, instance, owner):
478+
"""Descriptor protocol: accessor
479+
480+
:type instance: :class:`QueryParametersProperty`
481+
:param instance: instance owning the property (None if accessed via
482+
the class).
483+
484+
:type owner: type
485+
:param owner: the class owning the property.
486+
487+
:rtype: list of instances of classes derived from
488+
:class:`AbstractQueryParameter`.
489+
:returns: the descriptor, if accessed via the class, or the instance's
490+
query paramters.
491+
"""
492+
if instance is None:
493+
return self
494+
return list(instance._query_parameters)
495+
496+
def __set__(self, instance, value):
497+
"""Descriptor protocol: mutator
498+
499+
:type instance: :class:`QueryParametersProperty`
500+
:param instance: instance owning the property (None if accessed via
501+
the class).
502+
503+
:type value: list of instances of classes derived from
504+
:class:`AbstractQueryParameter`.
505+
:param value: new query parameters for the instance.
506+
"""
507+
if not all(isinstance(u, AbstractQueryParameter) for u in value):
508+
raise ValueError(
509+
"query parameters must be derived from AbstractQueryParameter")
510+
instance._query_parameters = tuple(value)

bigquery/google/cloud/bigquery/client.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@ def extract_table_to_storage(self, job_name, source, *destination_uris):
275275
return ExtractTableToStorageJob(job_name, source, destination_uris,
276276
client=self)
277277

278-
def run_async_query(self, job_name, query):
278+
def run_async_query(self, job_name, query,
279+
udf_resources=(), query_parameters=()):
279280
"""Construct a job for running a SQL query asynchronously.
280281
281282
See:
@@ -287,21 +288,47 @@ def run_async_query(self, job_name, query):
287288
:type query: str
288289
:param query: SQL query to be executed
289290
291+
:type udf_resources: tuple
292+
:param udf_resources: An iterable of
293+
:class:`google.cloud.bigquery._helpers.UDFResource`
294+
(empty by default)
295+
296+
:type query_parameters: tuple
297+
:param query_parameters:
298+
An iterable of
299+
:class:`google.cloud.bigquery._helpers.AbstractQueryParameter`
300+
(empty by default)
301+
290302
:rtype: :class:`google.cloud.bigquery.job.QueryJob`
291303
:returns: a new ``QueryJob`` instance
292304
"""
293-
return QueryJob(job_name, query, client=self)
305+
return QueryJob(job_name, query, client=self,
306+
udf_resources=udf_resources,
307+
query_parameters=query_parameters)
294308

295-
def run_sync_query(self, query):
309+
def run_sync_query(self, query, udf_resources=(), query_parameters=()):
296310
"""Run a SQL query synchronously.
297311
298312
:type query: str
299313
:param query: SQL query to be executed
300314
315+
:type udf_resources: tuple
316+
:param udf_resources: An iterable of
317+
:class:`google.cloud.bigquery._helpers.UDFResource`
318+
(empty by default)
319+
320+
:type query_parameters: tuple
321+
:param query_parameters:
322+
An iterable of
323+
:class:`google.cloud.bigquery._helpers.AbstractQueryParameter`
324+
(empty by default)
325+
301326
:rtype: :class:`google.cloud.bigquery.query.QueryResults`
302327
:returns: a new ``QueryResults`` instance
303328
"""
304-
return QueryResults(query, client=self)
329+
return QueryResults(query, client=self,
330+
udf_resources=udf_resources,
331+
query_parameters=query_parameters)
305332

306333

307334
# pylint: disable=unused-argument

0 commit comments

Comments
 (0)