Repository: googleapis/google-cloud-python

Issue:
Allow dryRun for jobs/queries
Not sure how to best do this; but would be great with the ability to trigger dryRuns of jobs to get an idea of the amount of data it will consume and validate the query.

https://cloud.google.com/bigquery/docs/reference/v2/jobs#configuration.dryRun

... essentially it could be just a parameter on the job like for example write_disposition; however it might make more sense to do one of:

a) Add a bool param on the job "begin" method to allow dry-runs
b) Add a dry_run method that mimics begin but does a dry-run



Discussion:
We already expose this feature for synchronous queries, which have a [`dry_run` property](https://github.com/GoogleCloudPlatform/google-cloud-python/blob/298b28ab931ead28ed913e19d18fae0b678ff9a2/google/cloud/bigquery/query.py#L224).  The [docs](https://cloud.google.com/bigquery/docs/reference/v2/jobs#configuration.dryRun) say, "Behavior of non-query jobs is undefined."  So, I'm guessing that this issue is really "Add `dry_run` support to asynchronous queries."

@thejens Looks like this would be very much like the work that @gene-telligent did in #2276:  add the `dry_run` typed property to the `QueryJob` class, and then ensure that it gets copied into the resource.


Resolving patch:
diff --git a/google/cloud/bigquery/job.py b/google/cloud/bigquery/job.py
--- a/google/cloud/bigquery/job.py
+++ b/google/cloud/bigquery/job.py
@@ -885,6 +885,7 @@ class _AsyncQueryConfiguration(object):
     _priority = None
     _use_query_cache = None
     _use_legacy_sql = None
+    _dry_run = None
     _write_disposition = None
     _maximum_billing_tier = None
     _maximum_bytes_billed = None
@@ -960,6 +961,12 @@ def __init__(self, name, query, client, udf_resources=()):
     reference/v2/jobs#configuration.query.useLegacySql
     """
 
+    dry_run = _TypedProperty('dry_run', bool)
+    """See:
+    https://cloud.google.com/bigquery/docs/\
+    reference/v2/jobs#configuration.query.dryRun
+    """
+
     write_disposition = WriteDisposition('write_disposition')
     """See:
     https://cloud.google.com/bigquery/docs/reference/v2/jobs#configuration.query.writeDisposition
@@ -988,10 +995,23 @@ def _destination_table_resource(self):
                 'tableId': self.destination.name,
             }
 
-    def _populate_config_resource(self, configuration):
-        """Helper for _build_resource: copy config properties to resource"""
+    def _populate_config_resource_booleans(self, configuration):
+        """Helper for _populate_config_resource."""
         if self.allow_large_results is not None:
             configuration['allowLargeResults'] = self.allow_large_results
+        if self.flatten_results is not None:
+            configuration['flattenResults'] = self.flatten_results
+        if self.use_query_cache is not None:
+            configuration['useQueryCache'] = self.use_query_cache
+        if self.use_legacy_sql is not None:
+            configuration['useLegacySql'] = self.use_legacy_sql
+        if self.dry_run is not None:
+            configuration['dryRun'] = self.dry_run
+
+    def _populate_config_resource(self, configuration):
+        """Helper for _build_resource: copy config properties to resource"""
+        self._populate_config_resource_booleans(configuration)
+
         if self.create_disposition is not None:
             configuration['createDisposition'] = self.create_disposition
         if self.default_dataset is not None:
@@ -1002,14 +1022,8 @@ def _populate_config_resource(self, configuration):
         if self.destination is not None:
             table_res = self._destination_table_resource()
             configuration['destinationTable'] = table_res
-        if self.flatten_results is not None:
-            configuration['flattenResults'] = self.flatten_results
         if self.priority is not None:
             configuration['priority'] = self.priority
-        if self.use_query_cache is not None:
-            configuration['useQueryCache'] = self.use_query_cache
-        if self.use_legacy_sql is not None:
-            configuration['useLegacySql'] = self.use_legacy_sql
         if self.write_disposition is not None:
             configuration['writeDisposition'] = self.write_disposition
         if self.maximum_billing_tier is not None:


Test patch:
diff --git a/unit_tests/bigquery/test_job.py b/unit_tests/bigquery/test_job.py
--- a/unit_tests/bigquery/test_job.py
+++ b/unit_tests/bigquery/test_job.py
@@ -1263,6 +1263,11 @@ def _verifyBooleanResourceProperties(self, job, config):
                              config['useLegacySql'])
         else:
             self.assertIsNone(job.use_legacy_sql)
+        if 'dryRun' in config:
+            self.assertEqual(job.dry_run,
+                             config['dryRun'])
+        else:
+            self.assertIsNone(job.dry_run)
 
     def _verifyIntegerResourceProperties(self, job, config):
         if 'maximumBillingTier' in config:
@@ -1339,6 +1344,7 @@ def test_ctor(self):
         self.assertIsNone(job.priority)
         self.assertIsNone(job.use_query_cache)
         self.assertIsNone(job.use_legacy_sql)
+        self.assertIsNone(job.dry_run)
         self.assertIsNone(job.write_disposition)
         self.assertIsNone(job.maximum_billing_tier)
         self.assertIsNone(job.maximum_bytes_billed)
@@ -1460,6 +1466,7 @@ def test_begin_w_alternate_client(self):
             'priority': 'INTERACTIVE',
             'useQueryCache': True,
             'useLegacySql': True,
+            'dryRun': True,
             'writeDisposition': 'WRITE_TRUNCATE',
             'maximumBillingTier': 4,
             'maximumBytesBilled': 123456
@@ -1482,6 +1489,7 @@ def test_begin_w_alternate_client(self):
         job.priority = 'INTERACTIVE'
         job.use_query_cache = True
         job.use_legacy_sql = True
+        job.dry_run = True
         job.write_disposition = 'WRITE_TRUNCATE'
         job.maximum_billing_tier = 4
         job.maximum_bytes_billed = 123456