Skip to content

Commit 177caa5

Browse files
committed
Ensure that configuration properties are copied when (re)loading jobs.
1 parent c29cba3 commit 177caa5

4 files changed

Lines changed: 255 additions & 17 deletions

File tree

bigquery/google/cloud/bigquery/_helpers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,18 @@ def to_api_repr(self):
662662
return resource
663663

664664

665+
def _query_param_from_api_repr(resource):
666+
"""Helper: construct concrete query parameter from JSON resource."""
667+
qp_type = resource['parameterType']
668+
if 'arrayType' in qp_type:
669+
klass = ArrayQueryParameter
670+
elif 'structTypes' in qp_type:
671+
klass = StructQueryParameter
672+
else:
673+
klass = ScalarQueryParameter
674+
return klass.from_api_repr(resource)
675+
676+
665677
class QueryParametersProperty(object):
666678
"""Custom property type, holding query parameter instances."""
667679

bigquery/google/cloud/bigquery/job.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,14 @@
2828
from google.cloud.bigquery.table import Table
2929
from google.cloud.bigquery.table import _build_schema_resource
3030
from google.cloud.bigquery.table import _parse_schema_resource
31+
from google.cloud.bigquery._helpers import ArrayQueryParameter
3132
from google.cloud.bigquery._helpers import QueryParametersProperty
33+
from google.cloud.bigquery._helpers import ScalarQueryParameter
34+
from google.cloud.bigquery._helpers import StructQueryParameter
35+
from google.cloud.bigquery._helpers import UDFResource
3236
from google.cloud.bigquery._helpers import UDFResourcesProperty
3337
from google.cloud.bigquery._helpers import _EnumProperty
38+
from google.cloud.bigquery._helpers import _query_param_from_api_repr
3439
from google.cloud.bigquery._helpers import _TypedProperty
3540

3641
_DONE_STATE = 'DONE'
@@ -58,6 +63,22 @@
5863
}
5964

6065

66+
def _bool_or_none(value):
67+
"""Helper: deserialize boolean value from JSON string."""
68+
if isinstance(value, bool):
69+
return value
70+
if value is not None:
71+
return value.lower() in ['t', 'true', '1']
72+
73+
74+
def _int_or_none(value):
75+
"""Helper: deserialize int value from JSON string."""
76+
if isinstance(value, int):
77+
return value
78+
if value is not None:
79+
return int(value)
80+
81+
6182
def _error_result_to_exception(error_result):
6283
"""Maps BigQuery error reasons to an exception.
6384
@@ -303,6 +324,10 @@ def _scrub_local_properties(self, cleaned):
303324
"""Helper: handle subclass properties in cleaned."""
304325
pass
305326

327+
def _copy_configuration_properties(self, configuration):
328+
"""Helper: assign subclass configuration properties in cleaned."""
329+
raise NotImplementedError("Abstract")
330+
306331
def _set_properties(self, api_response):
307332
"""Update properties from resource in body of ``api_response``
308333
@@ -322,6 +347,8 @@ def _set_properties(self, api_response):
322347

323348
self._properties.clear()
324349
self._properties.update(cleaned)
350+
configuration = cleaned['configuration'][self._JOB_TYPE]
351+
self._copy_configuration_properties(configuration)
325352

326353
# For Future interface
327354
self._set_future_result()
@@ -759,6 +786,28 @@ def _scrub_local_properties(self, cleaned):
759786
schema = cleaned.pop('schema', {'fields': ()})
760787
self.schema = _parse_schema_resource(schema)
761788

789+
def _copy_configuration_properties(self, configuration):
790+
"""Helper: assign subclass configuration properties in cleaned."""
791+
self.allow_jagged_rows = _bool_or_none(
792+
configuration.get('allowJaggedRows'))
793+
self.allow_quoted_newlines = _bool_or_none(
794+
configuration.get('allowQuotedNewlines'))
795+
self.autodetect = _bool_or_none(
796+
configuration.get('autodetect'))
797+
self.create_disposition = configuration.get('createDisposition')
798+
self.encoding = configuration.get('encoding')
799+
self.field_delimiter = configuration.get('fieldDelimiter')
800+
self.ignore_unknown_values = _bool_or_none(
801+
configuration.get('ignoreUnknownValues'))
802+
self.max_bad_records = _int_or_none(
803+
configuration.get('maxBadRecords'))
804+
self.null_marker = configuration.get('nullMarker')
805+
self.quote_character = configuration.get('quote')
806+
self.skip_leading_rows = _int_or_none(
807+
configuration.get('skipLeadingRows'))
808+
self.source_format = configuration.get('sourceFormat')
809+
self.write_disposition = configuration.get('writeDisposition')
810+
762811
@classmethod
763812
def from_api_repr(cls, resource, client):
764813
"""Factory: construct a job given its API representation
@@ -869,6 +918,11 @@ def _build_resource(self):
869918

870919
return resource
871920

921+
def _copy_configuration_properties(self, configuration):
922+
"""Helper: assign subclass configuration properties in cleaned."""
923+
self.create_disposition = configuration.get('createDisposition')
924+
self.write_disposition = configuration.get('writeDisposition')
925+
872926
@classmethod
873927
def from_api_repr(cls, resource, client):
874928
"""Factory: construct a job given its API representation
@@ -1002,6 +1056,14 @@ def _build_resource(self):
10021056

10031057
return resource
10041058

1059+
def _copy_configuration_properties(self, configuration):
1060+
"""Helper: assign subclass configuration properties in cleaned."""
1061+
self.compression = configuration.get('compression')
1062+
self.destination_format = configuration.get('destinationFormat')
1063+
self.field_delimiter = configuration.get('fieldDelimiter')
1064+
self.print_header = _bool_or_none(
1065+
configuration.get('printHeader'))
1066+
10051067
@classmethod
10061068
def from_api_repr(cls, resource, client):
10071069
"""Factory: construct a job given its API representation
@@ -1247,6 +1309,24 @@ def _scrub_local_properties(self, cleaned):
12471309
configuration = cleaned['configuration']['query']
12481310

12491311
self.query = configuration['query']
1312+
1313+
def _copy_configuration_properties(self, configuration):
1314+
"""Helper: assign subclass configuration properties in cleaned."""
1315+
self.allow_large_results = _bool_or_none(
1316+
configuration.get('allowLargeResults'))
1317+
self.flatten_results = _bool_or_none(
1318+
configuration.get('flattenResults'))
1319+
self.use_query_cache = _bool_or_none(
1320+
configuration.get('useQueryCache'))
1321+
self.use_legacy_sql = _bool_or_none(
1322+
configuration.get('useLegacySql'))
1323+
1324+
self.create_disposition = configuration.get('createDisposition')
1325+
self.priority = configuration.get('priority')
1326+
self.write_disposition = configuration.get('writeDisposition')
1327+
self.maximum_billing_tier = configuration.get('maximumBillingTier')
1328+
self.maximum_bytes_billed = configuration.get('maximumBytesBilled')
1329+
12501330
dest_remote = configuration.get('destinationTable')
12511331

12521332
if dest_remote is None:
@@ -1255,9 +1335,30 @@ def _scrub_local_properties(self, cleaned):
12551335
else:
12561336
dest_local = self._destination_table_resource()
12571337
if dest_remote != dest_local:
1258-
dataset = self._client.dataset(dest_remote['datasetId'])
1338+
project = dest_remote['projectId']
1339+
dataset = self._client.dataset(
1340+
dest_remote['datasetId'], project=project)
12591341
self.destination = dataset.table(dest_remote['tableId'])
12601342

1343+
def_ds = configuration.get('defaultDataset')
1344+
if def_ds is None:
1345+
if self.default_dataset is not None:
1346+
del self.default_dataset
1347+
else:
1348+
project = def_ds['projectId']
1349+
self.default_dataset = self._client.dataset(def_ds['datasetId'])
1350+
1351+
udf_resources = []
1352+
for udf_mapping in configuration.get(self._UDF_KEY, ()):
1353+
key_val, = udf_mapping.items()
1354+
udf_resources.append(UDFResource(key_val[0], key_val[1]))
1355+
self._udf_resources = udf_resources
1356+
1357+
self._query_parameters = [
1358+
_query_param_from_api_repr(mapping)
1359+
for mapping in configuration.get(self._QUERY_PARAMETERS_KEY, ())
1360+
]
1361+
12611362
@classmethod
12621363
def from_api_repr(cls, resource, client):
12631364
"""Factory: construct a job given its API representation

bigquery/tests/unit/test__helpers.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,6 +1528,82 @@ def test_to_api_repr_w_nested_struct(self):
15281528
self.assertEqual(param.to_api_repr(), EXPECTED)
15291529

15301530

1531+
class Test__query_param_from_api_repr(unittest.TestCase):
1532+
1533+
@staticmethod
1534+
def _call_fut(resource):
1535+
from google.cloud.bigquery._helpers import _query_param_from_api_repr
1536+
1537+
return _query_param_from_api_repr(resource)
1538+
1539+
def test_w_scalar(self):
1540+
from google.cloud.bigquery._helpers import ScalarQueryParameter
1541+
1542+
RESOURCE = {
1543+
'name': 'foo',
1544+
'parameterType': {'type': 'INT64'},
1545+
'parameterValue': {'value': '123'},
1546+
}
1547+
1548+
parameter = self._call_fut(RESOURCE)
1549+
1550+
self.assertIsInstance(parameter, ScalarQueryParameter)
1551+
self.assertEqual(parameter.name, 'foo')
1552+
self.assertEqual(parameter.type_, 'INT64')
1553+
self.assertEqual(parameter.value, 123)
1554+
1555+
def test_w_array(self):
1556+
from google.cloud.bigquery._helpers import ArrayQueryParameter
1557+
1558+
RESOURCE = {
1559+
'name': 'foo',
1560+
'parameterType': {
1561+
'type': 'ARRAY',
1562+
'arrayType': {'type': 'INT64'},
1563+
},
1564+
'parameterValue': {
1565+
'arrayValues': [
1566+
{'value': '123'},
1567+
]},
1568+
}
1569+
1570+
parameter = self._call_fut(RESOURCE)
1571+
1572+
self.assertIsInstance(parameter, ArrayQueryParameter)
1573+
self.assertEqual(parameter.name, 'foo')
1574+
self.assertEqual(parameter.array_type, 'INT64')
1575+
self.assertEqual(parameter.values, [123])
1576+
1577+
def test_w_struct(self):
1578+
from google.cloud.bigquery._helpers import StructQueryParameter
1579+
1580+
RESOURCE = {
1581+
'name': 'foo',
1582+
'parameterType': {
1583+
'type': 'STRUCT',
1584+
'structTypes': [
1585+
{'name': 'foo', 'type': {'type': 'STRING'}},
1586+
{'name': 'bar', 'type': {'type': 'INT64'}},
1587+
],
1588+
},
1589+
'parameterValue': {
1590+
'structValues': {
1591+
'foo': {'value': 'Foo'},
1592+
'bar': {'value': '123'},
1593+
}
1594+
},
1595+
}
1596+
1597+
parameter = self._call_fut(RESOURCE)
1598+
1599+
self.assertIsInstance(parameter, StructQueryParameter)
1600+
self.assertEqual(parameter.name, 'foo')
1601+
self.assertEqual(
1602+
parameter.struct_types, {'foo': 'STRING', 'bar': 'INT64'})
1603+
self.assertEqual(parameter.struct_values, {'foo': 'Foo', 'bar': 123})
1604+
1605+
1606+
15311607
class Test_QueryParametersProperty(unittest.TestCase):
15321608

15331609
@staticmethod

0 commit comments

Comments
 (0)