From 4f31fc3ca3498493b50932ad52ccf6a9ebf97ad8 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 7 Oct 2019 16:22:03 -0700 Subject: [PATCH 1/2] feat(bigquery): add script statistics to job resource --- bigquery/google/cloud/bigquery/job.py | 88 +++++++++++++++++++++++++++ bigquery/tests/unit/test_job.py | 40 ++++++++++++ 2 files changed, 128 insertions(+) diff --git a/bigquery/google/cloud/bigquery/job.py b/bigquery/google/cloud/bigquery/job.py index b15189651d3c..624e3856b54c 100644 --- a/bigquery/google/cloud/bigquery/job.py +++ b/bigquery/google/cloud/bigquery/job.py @@ -344,6 +344,15 @@ def parent_job_id(self): """ return _helpers._get_sub_prop(self._properties, ["statistics", "parentJobId"]) + @property + def script_statistics(self): + resource = _helpers._get_sub_prop( + self._properties, ["statistics", "scriptStatistics"] + ) + if resource is None: + return None + return ScriptStatistics(resource) + @property def num_child_jobs(self): """The number of child jobs executed. @@ -3456,3 +3465,82 @@ def from_api_repr(cls, resource, client): resource["jobReference"] = job_ref_properties job._properties = resource return job + + +class ScriptStackFrame(object): + """Stack frame showing the line/column/procedure name where the current + evaluation happened. + + Args: + resource (Map[str, Any]): + JSON representation of object. + """ + + def __init__(self, resource): + self._properties = resource + + @property + def procedure_id(self): + """str: Name of the active procedure, empty if in a top-level + script. + """ + return self._properties.get("procedureId") + + @property + def text(self): + """str: Text of the current statement/expression.""" + return self._properties.get("text") + + @property + def start_line(self): + """int: One-based start line.""" + return _helpers._int_or_none(self._properties.get("startLine")) + + @property + def start_column(self): + """int: One-based start column.""" + return _helpers._int_or_none(self._properties.get("startColumn")) + + @property + def end_line(self): + """int: One-based end line.""" + return _helpers._int_or_none(self._properties.get("endLine")) + + @property + def end_column(self): + """int: One-based end column.""" + return _helpers._int_or_none(self._properties.get("endColumn")) + + +class ScriptStatistics(object): + """Statistics for a child job of a script. + + Args: + resource (Map[str, Any]): + JSON representation of object. + """ + + def __init__(self, resource): + self._properties = resource + + @property + def stack_frames(self): + """List[ScriptStackFrame]: Stack trace where the current evaluation + happened. + + Shows line/column/procedure name of each frame on the stack at the + point where the current evaluation happened. + + The leaf frame is first, the primary script is last. + """ + return [ + ScriptStackFrame(frame) for frame in self._properties.get("stackFrames", []) + ] + + @property + def evaluation_kind(self): + """str: Indicates the type of child job. + + Possible values include ``STATEMENT`` and ``EXPRESSION``. + """ + return self._properties.get("evaluationKind") diff --git a/bigquery/tests/unit/test_job.py b/bigquery/tests/unit/test_job.py index a46004b1a97f..c6916b293a76 100644 --- a/bigquery/tests/unit/test_job.py +++ b/bigquery/tests/unit/test_job.py @@ -276,6 +276,46 @@ def test_parent_job_id(self): job._properties["statistics"] = {"parentJobId": "parent-job-123"} self.assertEqual(job.parent_job_id, "parent-job-123") + def test_script_statistics(self): + client = _make_client(project=self.PROJECT) + job = self._make_one(self.JOB_ID, client) + + self.assertIsNone(job.script_statistics) + job._properties["statistics"] = { + "scriptStatistics": { + "evaluationKind": "EXPRESSION", + "stackFrames": [ + { + "procedureId": "some-procedure", + "startLine": 5, + "startColumn": 29, + "endLine": 9, + "endColumn": 14, + "text": "QUERY TEXT", + }, + {}, + ], + } + } + script_stats = job.script_statistics + self.assertEqual(script_stats.evaluation_kind, "EXPRESSION") + stack_frames = script_stats.stack_frames + self.assertEqual(len(stack_frames), 2) + stack_frame = stack_frames[0] + self.assertEqual(stack_frame.procedure_id, "some-procedure") + self.assertEqual(stack_frame.start_line, 5) + self.assertEqual(stack_frame.start_column, 29) + self.assertEqual(stack_frame.end_line, 9) + self.assertEqual(stack_frame.end_column, 14) + self.assertEqual(stack_frame.text, "QUERY TEXT") + stack_frame = stack_frames[1] + self.assertIsNone(stack_frame.procedure_id) + self.assertIsNone(stack_frame.start_line) + self.assertIsNone(stack_frame.start_column) + self.assertIsNone(stack_frame.end_line) + self.assertIsNone(stack_frame.end_column) + self.assertIsNone(stack_frame.text) + def test_num_child_jobs(self): client = _make_client(project=self.PROJECT) job = self._make_one(self.JOB_ID, client) From 8f00cc4617bcc599ce9cadc72007398809c4194a Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Wed, 9 Oct 2019 13:01:56 -0700 Subject: [PATCH 2/2] add explicit unit test coverage for the ScriptStackFrame and ScriptStatistics classes --- bigquery/google/cloud/bigquery/job.py | 5 +- bigquery/tests/unit/test_job.py | 101 +++++++++++++++++++++++--- 2 files changed, 92 insertions(+), 14 deletions(-) diff --git a/bigquery/google/cloud/bigquery/job.py b/bigquery/google/cloud/bigquery/job.py index 624e3856b54c..6768e45fbbcf 100644 --- a/bigquery/google/cloud/bigquery/job.py +++ b/bigquery/google/cloud/bigquery/job.py @@ -3481,8 +3481,9 @@ def __init__(self, resource): @property def procedure_id(self): - """str: Name of the active procedure, empty if in a top-level - script. + """Optional[str]: Name of the active procedure. + + Omitted if in a top-level script. """ return self._properties.get("procedureId") diff --git a/bigquery/tests/unit/test_job.py b/bigquery/tests/unit/test_job.py index c6916b293a76..9710085105c4 100644 --- a/bigquery/tests/unit/test_job.py +++ b/bigquery/tests/unit/test_job.py @@ -286,35 +286,26 @@ def test_script_statistics(self): "evaluationKind": "EXPRESSION", "stackFrames": [ { - "procedureId": "some-procedure", "startLine": 5, "startColumn": 29, "endLine": 9, "endColumn": 14, "text": "QUERY TEXT", - }, - {}, + } ], } } script_stats = job.script_statistics self.assertEqual(script_stats.evaluation_kind, "EXPRESSION") stack_frames = script_stats.stack_frames - self.assertEqual(len(stack_frames), 2) + self.assertEqual(len(stack_frames), 1) stack_frame = stack_frames[0] - self.assertEqual(stack_frame.procedure_id, "some-procedure") + self.assertIsNone(stack_frame.procedure_id) self.assertEqual(stack_frame.start_line, 5) self.assertEqual(stack_frame.start_column, 29) self.assertEqual(stack_frame.end_line, 9) self.assertEqual(stack_frame.end_column, 14) self.assertEqual(stack_frame.text, "QUERY TEXT") - stack_frame = stack_frames[1] - self.assertIsNone(stack_frame.procedure_id) - self.assertIsNone(stack_frame.start_line) - self.assertIsNone(stack_frame.start_column) - self.assertIsNone(stack_frame.end_line) - self.assertIsNone(stack_frame.end_column) - self.assertIsNone(stack_frame.text) def test_num_child_jobs(self): client = _make_client(project=self.PROJECT) @@ -5379,6 +5370,92 @@ def test_end(self): self.assertEqual(entry.end.strftime(_RFC3339_MICROS), self.END_RFC3339_MICROS) +class TestScriptStackFrame(unittest.TestCase, _Base): + def _make_one(self, resource): + from google.cloud.bigquery.job import ScriptStackFrame + + return ScriptStackFrame(resource) + + def test_procedure_id(self): + frame = self._make_one({"procedureId": "some-procedure"}) + self.assertEqual(frame.procedure_id, "some-procedure") + del frame._properties["procedureId"] + self.assertIsNone(frame.procedure_id) + + def test_start_line(self): + frame = self._make_one({"startLine": 5}) + self.assertEqual(frame.start_line, 5) + frame._properties["startLine"] = "5" + self.assertEqual(frame.start_line, 5) + + def test_start_column(self): + frame = self._make_one({"startColumn": 29}) + self.assertEqual(frame.start_column, 29) + frame._properties["startColumn"] = "29" + self.assertEqual(frame.start_column, 29) + + def test_end_line(self): + frame = self._make_one({"endLine": 9}) + self.assertEqual(frame.end_line, 9) + frame._properties["endLine"] = "9" + self.assertEqual(frame.end_line, 9) + + def test_end_column(self): + frame = self._make_one({"endColumn": 14}) + self.assertEqual(frame.end_column, 14) + frame._properties["endColumn"] = "14" + self.assertEqual(frame.end_column, 14) + + def test_text(self): + frame = self._make_one({"text": "QUERY TEXT"}) + self.assertEqual(frame.text, "QUERY TEXT") + + +class TestScriptStatistics(unittest.TestCase, _Base): + def _make_one(self, resource): + from google.cloud.bigquery.job import ScriptStatistics + + return ScriptStatistics(resource) + + def test_evalutation_kind(self): + stats = self._make_one({"evaluationKind": "EXPRESSION"}) + self.assertEqual(stats.evaluation_kind, "EXPRESSION") + self.assertEqual(stats.stack_frames, []) + + def test_stack_frames(self): + stats = self._make_one( + { + "stackFrames": [ + { + "procedureId": "some-procedure", + "startLine": 5, + "startColumn": 29, + "endLine": 9, + "endColumn": 14, + "text": "QUERY TEXT", + }, + {}, + ] + } + ) + stack_frames = stats.stack_frames + self.assertEqual(len(stack_frames), 2) + stack_frame = stack_frames[0] + self.assertEqual(stack_frame.procedure_id, "some-procedure") + self.assertEqual(stack_frame.start_line, 5) + self.assertEqual(stack_frame.start_column, 29) + self.assertEqual(stack_frame.end_line, 9) + self.assertEqual(stack_frame.end_column, 14) + self.assertEqual(stack_frame.text, "QUERY TEXT") + stack_frame = stack_frames[1] + self.assertIsNone(stack_frame.procedure_id) + self.assertIsNone(stack_frame.start_line) + self.assertIsNone(stack_frame.start_column) + self.assertIsNone(stack_frame.end_line) + self.assertIsNone(stack_frame.end_column) + self.assertIsNone(stack_frame.text) + + class TestTimelineEntry(unittest.TestCase, _Base): ELAPSED_MS = 101 ACTIVE_UNITS = 50