Skip to content

Commit

Permalink
Add support for query parameters (googleapis#2776)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
tseaver authored Dec 2, 2016
1 parent 6224c08 commit 92e5119
Show file tree
Hide file tree
Showing 11 changed files with 1,158 additions and 98 deletions.
3 changes: 3 additions & 0 deletions bigquery/google/cloud/bigquery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"""


from google.cloud.bigquery._helpers import ArrayQueryParameter
from google.cloud.bigquery._helpers import ScalarQueryParameter
from google.cloud.bigquery._helpers import StructQueryParameter
from google.cloud.bigquery.client import Client
from google.cloud.bigquery.dataset import AccessGrant
from google.cloud.bigquery.dataset import Dataset
Expand Down
285 changes: 275 additions & 10 deletions bigquery/google/cloud/bigquery/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

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

from collections import OrderedDict

from google.cloud._helpers import _datetime_from_microseconds
from google.cloud._helpers import _date_from_iso8601_date

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


def _build_udf_resources(resources):
class AbstractQueryParameter(object):
"""Base class for named / positional query parameters.
"""
:type resources: sequence of :class:`UDFResource`
:param resources: fields to be appended.
@classmethod
def from_api_repr(cls, resource):
"""Factory: construct paramter from JSON resource.
:type resource: dict
:param resource: JSON mapping of parameter
:rtype: :class:`ScalarQueryParameter`
"""
raise NotImplementedError

def to_api_repr(self):
"""Construct JSON API representation for the parameter.
:rtype: dict
"""
raise NotImplementedError


:rtype: mapping
:returns: a mapping describing userDefinedFunctionResources for the query.
class ScalarQueryParameter(AbstractQueryParameter):
"""Named / positional query parameters for scalar values.
:type name: str or None
:param name: Parameter name, used via ``@foo`` syntax. If None, the
paramter can only be addressed via position (``?``).
:type type_: str
:param type_: name of parameter type. One of `'STRING'`, `'INT64'`,
`'FLOAT64'`, `'BOOL'`, `'TIMESTAMP'`, or `'DATE'`.
:type value: str, int, float, bool, :class:`datetime.datetime`, or
:class:`datetime.date`.
:param value: the scalar parameter value.
"""
udfs = []
for resource in resources:
udf = {resource.udf_type: resource.value}
udfs.append(udf)
return udfs
def __init__(self, name, type_, value):
self.name = name
self.type_ = type_
self.value = value

@classmethod
def positional(cls, type_, value):
"""Factory for positional paramters.
:type type_: str
:param type_: name of paramter type. One of `'STRING'`, `'INT64'`,
`'FLOAT64'`, `'BOOL'`, `'TIMESTAMP'`, or `'DATE'`.
:type value: str, int, float, bool, :class:`datetime.datetime`, or
:class:`datetime.date`.
:param value: the scalar parameter value.
:rtype: :class:`ScalarQueryParameter`
:returns: instance without name
"""
return cls(None, type_, value)

@classmethod
def from_api_repr(cls, resource):
"""Factory: construct paramter from JSON resource.
:type resource: dict
:param resource: JSON mapping of parameter
:rtype: :class:`ScalarQueryParameter`
:returns: instance
"""
name = resource.get('name')
type_ = resource['parameterType']['type']
value = resource['parameterValue']['value']
converted = _CELLDATA_FROM_JSON[type_](value, None)
return cls(name, type_, converted)

def to_api_repr(self):
"""Construct JSON API representation for the parameter.
:rtype: dict
:returns: JSON mapping
"""
resource = {
'parameterType': {
'type': self.type_,
},
'parameterValue': {
'value': self.value,
},
}
if self.name is not None:
resource['name'] = self.name
return resource


class ArrayQueryParameter(AbstractQueryParameter):
"""Named / positional query parameters for array values.
:type name: str or None
:param name: Parameter name, used via ``@foo`` syntax. If None, the
paramter can only be addressed via position (``?``).
:type array_type: str
:param array_type:
name of type of array elements. One of `'STRING'`, `'INT64'`,
`'FLOAT64'`, `'BOOL'`, `'TIMESTAMP'`, or `'DATE'`.
:type values: list of appropriate scalar type.
:param values: the parameter array values.
"""
def __init__(self, name, array_type, values):
self.name = name
self.array_type = array_type
self.values = values

@classmethod
def positional(cls, array_type, values):
"""Factory for positional paramters.
:type array_type: str
:param array_type:
name of type of array elements. One of `'STRING'`, `'INT64'`,
`'FLOAT64'`, `'BOOL'`, `'TIMESTAMP'`, or `'DATE'`.
:type values: list of appropriate scalar type
:param values: the parameter array values.
:rtype: :class:`ArrayQueryParameter`
:returns: instance without name
"""
return cls(None, array_type, values)

@classmethod
def from_api_repr(cls, resource):
"""Factory: construct paramter from JSON resource.
:type resource: dict
:param resource: JSON mapping of parameter
:rtype: :class:`ArrayQueryParameter`
:returns: instance
"""
name = resource.get('name')
array_type = resource['parameterType']['arrayType']
values = resource['parameterValue']['arrayValues']
converted = [
_CELLDATA_FROM_JSON[array_type](value, None) for value in values]
return cls(name, array_type, converted)

def to_api_repr(self):
"""Construct JSON API representation for the parameter.
:rtype: dict
:returns: JSON mapping
"""
resource = {
'parameterType': {
'arrayType': self.array_type,
},
'parameterValue': {
'arrayValues': self.values,
},
}
if self.name is not None:
resource['name'] = self.name
return resource


class StructQueryParameter(AbstractQueryParameter):
"""Named / positional query parameters for struct values.
:type name: str or None
:param name: Parameter name, used via ``@foo`` syntax. If None, the
paramter can only be addressed via position (``?``).
:type sub_params: tuple of :class:`ScalarQueryParameter`
:param sub_params: the sub-parameters for the struct
"""
def __init__(self, name, *sub_params):
self.name = name
self.struct_types = OrderedDict(
(sub.name, sub.type_) for sub in sub_params)
self.struct_values = {sub.name: sub.value for sub in sub_params}

@classmethod
def positional(cls, *sub_params):
"""Factory for positional paramters.
:type sub_params: tuple of :class:`ScalarQueryParameter`
:param sub_params: the sub-parameters for the struct
:rtype: :class:`StructQueryParameter`
:returns: instance without name
"""
return cls(None, *sub_params)

@classmethod
def from_api_repr(cls, resource):
"""Factory: construct paramter from JSON resource.
:type resource: dict
:param resource: JSON mapping of parameter
:rtype: :class:`StructQueryParameter`
:returns: instance
"""
name = resource.get('name')
instance = cls(name)
types = instance.struct_types
for item in resource['parameterType']['structTypes']:
types[item['name']] = item['type']
struct_values = resource['parameterValue']['structValues']
for key, value in struct_values.items():
converted = _CELLDATA_FROM_JSON[types[key]](value, None)
instance.struct_values[key] = converted
return instance

def to_api_repr(self):
"""Construct JSON API representation for the parameter.
:rtype: dict
:returns: JSON mapping
"""
types = [
{'name': key, 'type': value}
for key, value in self.struct_types.items()
]
resource = {
'parameterType': {
'structTypes': types,
},
'parameterValue': {
'structValues': self.struct_values,
},
}
if self.name is not None:
resource['name'] = self.name
return resource


class QueryParametersProperty(object):
"""Custom property type, holding query parameter instances."""

def __get__(self, instance, owner):
"""Descriptor protocol: accessor
:type instance: :class:`QueryParametersProperty`
:param instance: instance owning the property (None if accessed via
the class).
:type owner: type
:param owner: the class owning the property.
:rtype: list of instances of classes derived from
:class:`AbstractQueryParameter`.
:returns: the descriptor, if accessed via the class, or the instance's
query paramters.
"""
if instance is None:
return self
return list(instance._query_parameters)

def __set__(self, instance, value):
"""Descriptor protocol: mutator
:type instance: :class:`QueryParametersProperty`
:param instance: instance owning the property (None if accessed via
the class).
:type value: list of instances of classes derived from
:class:`AbstractQueryParameter`.
:param value: new query parameters for the instance.
"""
if not all(isinstance(u, AbstractQueryParameter) for u in value):
raise ValueError(
"query parameters must be derived from AbstractQueryParameter")
instance._query_parameters = tuple(value)
35 changes: 31 additions & 4 deletions bigquery/google/cloud/bigquery/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,8 @@ def extract_table_to_storage(self, job_name, source, *destination_uris):
return ExtractTableToStorageJob(job_name, source, destination_uris,
client=self)

def run_async_query(self, job_name, query):
def run_async_query(self, job_name, query,
udf_resources=(), query_parameters=()):
"""Construct a job for running a SQL query asynchronously.
See:
Expand All @@ -287,21 +288,47 @@ def run_async_query(self, job_name, query):
:type query: str
:param query: SQL query to be executed
:type udf_resources: tuple
:param udf_resources: An iterable of
:class:`google.cloud.bigquery._helpers.UDFResource`
(empty by default)
:type query_parameters: tuple
:param query_parameters:
An iterable of
:class:`google.cloud.bigquery._helpers.AbstractQueryParameter`
(empty by default)
:rtype: :class:`google.cloud.bigquery.job.QueryJob`
:returns: a new ``QueryJob`` instance
"""
return QueryJob(job_name, query, client=self)
return QueryJob(job_name, query, client=self,
udf_resources=udf_resources,
query_parameters=query_parameters)

def run_sync_query(self, query):
def run_sync_query(self, query, udf_resources=(), query_parameters=()):
"""Run a SQL query synchronously.
:type query: str
:param query: SQL query to be executed
:type udf_resources: tuple
:param udf_resources: An iterable of
:class:`google.cloud.bigquery._helpers.UDFResource`
(empty by default)
:type query_parameters: tuple
:param query_parameters:
An iterable of
:class:`google.cloud.bigquery._helpers.AbstractQueryParameter`
(empty by default)
:rtype: :class:`google.cloud.bigquery.query.QueryResults`
:returns: a new ``QueryResults`` instance
"""
return QueryResults(query, client=self)
return QueryResults(query, client=self,
udf_resources=udf_resources,
query_parameters=query_parameters)


# pylint: disable=unused-argument
Expand Down
Loading

0 comments on commit 92e5119

Please sign in to comment.