Repository: googleapis/python-bigquery

Issue:
feat: allow QueryJob.result() to be called on a dryRun query
Attempting to call `result()` on a dryRun query results in the following exception.

```
    def test_dry_run(bigquery_client: bigquery.Client, query_api_method: str, scalars_table_multi_location: Tuple[str, str]):
        location, full_table_id = scalars_table_multi_location
        query_config = bigquery.QueryJobConfig()
        query_config.dry_run = True
    
        query_string = f"SELECT * FROM {full_table_id}"
        query_job = bigquery_client.query(query_string, location=location, job_config=query_config, api_method=query_api_method)
>       query_job.result()

tests/system/test_query.py:430: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
google/cloud/bigquery/job/query.py:1372: in result
    do_get_result()
../python-api-core/google/api_core/retry.py:283: in retry_wrapped_func
    return retry_target(
../python-api-core/google/api_core/retry.py:190: in retry_target
    return target()
google/cloud/bigquery/job/query.py:1362: in do_get_result
    super(QueryJob, self).result(retry=retry, timeout=timeout)
google/cloud/bigquery/job/base.py:713: in result
    return super(_AsyncJob, self).result(timeout=timeout, **kwargs)
../python-api-core/google/api_core/future/polling.py:137: in result
    raise self._exception
google/cloud/bigquery/job/query.py:1233: in _done_or_raise
    self._reload_query_results(retry=retry, timeout=transport_timeout)
google/cloud/bigquery/job/query.py:1214: in _reload_query_results
    self._query_results = self._client._get_query_results(
google/cloud/bigquery/client.py:1835: in _get_query_results
    resource = self._call_api(
google/cloud/bigquery/client.py:732: in _call_api
    return call()
../python-api-core/google/api_core/retry.py:283: in retry_wrapped_func
    return retry_target(
../python-api-core/google/api_core/retry.py:190: in retry_target
    return target()

...

E           google.api_core.exceptions.NotFound: 404 GET https://bigquery.googleapis.com/bigquery/v2/projects/swast-scratch/queries/None?maxResults=0&location=asia-northeast1&prettyPrint=false: Not found: Job swast-scratch:asia-northeast1.None
E           
E           (job ID: None)
E           
E                                            -----Query Job SQL Follows-----                                 
E           
E               |    .    |    .    |    .    |    .    |    .    |    .    |    .    |    .    |    .    |
E              1:SELECT * FROM swast-scratch.python_bigquery_tests_system_20211008194804_6e302d_tokyo.scalars
E               |    .    |    .    |    .    |    .    |    .    |    .    |    .    |    .    |    .    |

../python-cloud-core/google/cloud/_http.py:479: NotFound
```

This is because the QueryJob tries to fetch query results and/or job state, but there is no job ID in a dry run query response.

IMO, if we know something is a dryRun response, `resullt()` should do nothing.


Discussion:

Resolving patch:
diff --git a/google/cloud/bigquery/job/query.py b/google/cloud/bigquery/job/query.py
--- a/google/cloud/bigquery/job/query.py
+++ b/google/cloud/bigquery/job/query.py
@@ -1318,6 +1318,8 @@ def result(
                 If Non-``None`` and non-default ``job_retry`` is
                 provided and the job is not retryable.
         """
+        if self.dry_run:
+            return _EmptyRowIterator()
         try:
             retry_do_query = getattr(self, "_retry_do_query", None)
             if retry_do_query is not None:


Test patch:
diff --git a/tests/unit/job/test_query.py b/tests/unit/job/test_query.py
--- a/tests/unit/job/test_query.py
+++ b/tests/unit/job/test_query.py
@@ -26,6 +26,7 @@
 
 from google.cloud.bigquery.client import _LIST_ROWS_FROM_QUERY_RESULTS_FIELDS
 import google.cloud.bigquery.query
+from google.cloud.bigquery.table import _EmptyRowIterator
 
 from ..helpers import make_connection
 
@@ -989,6 +990,19 @@ def test_result(self):
             [query_results_call, query_results_call, reload_call, query_page_call]
         )
 
+    def test_result_dry_run(self):
+        job_resource = self._make_resource(started=True, location="EU")
+        job_resource["configuration"]["dryRun"] = True
+        conn = make_connection()
+        client = _make_client(self.PROJECT, connection=conn)
+        job = self._get_target_class().from_api_repr(job_resource, client)
+
+        result = job.result()
+
+        calls = conn.api_request.mock_calls
+        self.assertIsInstance(result, _EmptyRowIterator)
+        self.assertEqual(calls, [])
+
     def test_result_with_done_job_calls_get_query_results(self):
         query_resource_done = {
             "jobComplete": True,