Skip to content

Commit 6e6cd39

Browse files
plamuttswast
authored andcommitted
feat(bigquery): store QueryJob to destination var on error (#9245)
1 parent c037a51 commit 6e6cd39

File tree

4 files changed

+66
-11
lines changed

4 files changed

+66
-11
lines changed

bigquery/google/cloud/bigquery/job.py

+2
Original file line numberDiff line numberDiff line change
@@ -2903,6 +2903,7 @@ def _begin(self, client=None, retry=DEFAULT_RETRY):
29032903
super(QueryJob, self)._begin(client=client, retry=retry)
29042904
except exceptions.GoogleCloudError as exc:
29052905
exc.message += self._format_for_exception(self.query, self.job_id)
2906+
exc.query_job = self
29062907
raise
29072908

29082909
def result(
@@ -2945,6 +2946,7 @@ def result(
29452946
)
29462947
except exceptions.GoogleCloudError as exc:
29472948
exc.message += self._format_for_exception(self.query, self.job_id)
2949+
exc.query_job = self
29482950
raise
29492951

29502952
# If the query job is complete but there are no query results, this was

bigquery/google/cloud/bigquery/magics.py

+27-9
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
2929
* ``<destination_var>`` (optional, line argument):
3030
variable to store the query results. The results are not displayed if
31-
this parameter is used.
31+
this parameter is used. If an error occurs during the query execution,
32+
the corresponding ``QueryJob`` instance (if available) is stored in
33+
the variable instead.
3234
* ``--project <project>`` (optional, line argument):
3335
Project to use for running the query. Defaults to the context
3436
:attr:`~google.cloud.bigquery.magics.Context.project`.
@@ -267,13 +269,29 @@ def default_query_job_config(self, value):
267269
context = Context()
268270

269271

270-
def _print_error(error, destination_var=None):
272+
def _handle_error(error, destination_var=None):
273+
"""Process a query execution error.
274+
275+
Args:
276+
error (Exception):
277+
An exception that ocurred during the query exectution.
278+
destination_var (Optional[str]):
279+
The name of the IPython session variable to store the query job.
280+
"""
271281
if destination_var:
272-
print(
273-
"Could not save output to variable '{}'.".format(destination_var),
274-
file=sys.stderr,
275-
)
276-
print("\nERROR:\n", error, file=sys.stderr)
282+
query_job = getattr(error, "query_job", None)
283+
284+
if query_job is not None:
285+
IPython.get_ipython().push({destination_var: query_job})
286+
else:
287+
# this is the case when previewing table rows by providing just
288+
# table ID to cell magic
289+
print(
290+
"Could not save output to variable '{}'.".format(destination_var),
291+
file=sys.stderr,
292+
)
293+
294+
print("\nERROR:\n", str(error), file=sys.stderr)
277295

278296

279297
def _run_query(client, query, job_config=None):
@@ -452,7 +470,7 @@ def _cell_magic(line, query):
452470
try:
453471
rows = client.list_rows(query, max_results=max_results)
454472
except Exception as ex:
455-
_print_error(str(ex), args.destination_var)
473+
_handle_error(ex, args.destination_var)
456474
return
457475

458476
result = rows.to_dataframe(bqstorage_client=bqstorage_client)
@@ -476,7 +494,7 @@ def _cell_magic(line, query):
476494
try:
477495
query_job = _run_query(client, query, job_config=job_config)
478496
except Exception as ex:
479-
_print_error(str(ex), args.destination_var)
497+
_handle_error(ex, args.destination_var)
480498
return
481499

482500
if not args.verbose:

bigquery/tests/unit/test_job.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -4337,8 +4337,10 @@ def test_result_error(self):
43374337
self.assertIsInstance(exc_info.exception, exceptions.GoogleCloudError)
43384338
self.assertEqual(exc_info.exception.code, http_client.BAD_REQUEST)
43394339

4340-
full_text = str(exc_info.exception)
4340+
exc_job_instance = getattr(exc_info.exception, "query_job", None)
4341+
self.assertIs(exc_job_instance, job)
43414342

4343+
full_text = str(exc_info.exception)
43424344
assert job.job_id in full_text
43434345
assert "Query Job SQL Follows" in full_text
43444346

@@ -4370,8 +4372,10 @@ def test__begin_error(self):
43704372
self.assertIsInstance(exc_info.exception, exceptions.GoogleCloudError)
43714373
self.assertEqual(exc_info.exception.code, http_client.BAD_REQUEST)
43724374

4373-
full_text = str(exc_info.exception)
4375+
exc_job_instance = getattr(exc_info.exception, "query_job", None)
4376+
self.assertIs(exc_job_instance, job)
43744377

4378+
full_text = str(exc_info.exception)
43754379
assert job.job_id in full_text
43764380
assert "Query Job SQL Follows" in full_text
43774381

bigquery/tests/unit/test_magics.py

+31
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,37 @@ def test_bigquery_magic_dryrun_option_saves_query_job_to_variable():
902902
assert isinstance(q_job, job.QueryJob)
903903

904904

905+
@pytest.mark.usefixtures("ipython_interactive")
906+
def test_bigquery_magic_saves_query_job_to_variable_on_error():
907+
ip = IPython.get_ipython()
908+
ip.extension_manager.load_extension("google.cloud.bigquery")
909+
magics.context.credentials = mock.create_autospec(
910+
google.auth.credentials.Credentials, instance=True
911+
)
912+
913+
client_query_patch = mock.patch(
914+
"google.cloud.bigquery.client.Client.query", autospec=True
915+
)
916+
917+
query_job = mock.create_autospec(job.QueryJob, instance=True)
918+
exception = Exception("Unexpected SELECT")
919+
exception.query_job = query_job
920+
query_job.result.side_effect = exception
921+
922+
sql = "SELECT SELECT 17 AS num"
923+
924+
assert "result" not in ip.user_ns
925+
926+
with client_query_patch as client_query_mock:
927+
client_query_mock.return_value = query_job
928+
return_value = ip.run_cell_magic("bigquery", "result", sql)
929+
930+
assert return_value is None
931+
assert "result" in ip.user_ns
932+
result = ip.user_ns["result"]
933+
assert isinstance(result, job.QueryJob)
934+
935+
905936
@pytest.mark.usefixtures("ipython_interactive")
906937
def test_bigquery_magic_w_maximum_bytes_billed_invalid():
907938
ip = IPython.get_ipython()

0 commit comments

Comments
 (0)