Skip to content

Commit 1d4a547

Browse files
authored
feat(bigquery): add script statistics to job resource (#9428)
* feat(bigquery): add script statistics to job resource * add explicit unit test coverage for the ScriptStackFrame and ScriptStatistics classes
1 parent 32412ba commit 1d4a547

2 files changed

Lines changed: 206 additions & 0 deletions

File tree

bigquery/google/cloud/bigquery/job.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,15 @@ def parent_job_id(self):
344344
"""
345345
return _helpers._get_sub_prop(self._properties, ["statistics", "parentJobId"])
346346

347+
@property
348+
def script_statistics(self):
349+
resource = _helpers._get_sub_prop(
350+
self._properties, ["statistics", "scriptStatistics"]
351+
)
352+
if resource is None:
353+
return None
354+
return ScriptStatistics(resource)
355+
347356
@property
348357
def num_child_jobs(self):
349358
"""The number of child jobs executed.
@@ -3456,3 +3465,83 @@ def from_api_repr(cls, resource, client):
34563465
resource["jobReference"] = job_ref_properties
34573466
job._properties = resource
34583467
return job
3468+
3469+
3470+
class ScriptStackFrame(object):
3471+
"""Stack frame showing the line/column/procedure name where the current
3472+
evaluation happened.
3473+
3474+
Args:
3475+
resource (Map[str, Any]):
3476+
JSON representation of object.
3477+
"""
3478+
3479+
def __init__(self, resource):
3480+
self._properties = resource
3481+
3482+
@property
3483+
def procedure_id(self):
3484+
"""Optional[str]: Name of the active procedure.
3485+
3486+
Omitted if in a top-level script.
3487+
"""
3488+
return self._properties.get("procedureId")
3489+
3490+
@property
3491+
def text(self):
3492+
"""str: Text of the current statement/expression."""
3493+
return self._properties.get("text")
3494+
3495+
@property
3496+
def start_line(self):
3497+
"""int: One-based start line."""
3498+
return _helpers._int_or_none(self._properties.get("startLine"))
3499+
3500+
@property
3501+
def start_column(self):
3502+
"""int: One-based start column."""
3503+
return _helpers._int_or_none(self._properties.get("startColumn"))
3504+
3505+
@property
3506+
def end_line(self):
3507+
"""int: One-based end line."""
3508+
return _helpers._int_or_none(self._properties.get("endLine"))
3509+
3510+
@property
3511+
def end_column(self):
3512+
"""int: One-based end column."""
3513+
return _helpers._int_or_none(self._properties.get("endColumn"))
3514+
3515+
3516+
class ScriptStatistics(object):
3517+
"""Statistics for a child job of a script.
3518+
3519+
Args:
3520+
resource (Map[str, Any]):
3521+
JSON representation of object.
3522+
"""
3523+
3524+
def __init__(self, resource):
3525+
self._properties = resource
3526+
3527+
@property
3528+
def stack_frames(self):
3529+
"""List[ScriptStackFrame]: Stack trace where the current evaluation
3530+
happened.
3531+
3532+
Shows line/column/procedure name of each frame on the stack at the
3533+
point where the current evaluation happened.
3534+
3535+
The leaf frame is first, the primary script is last.
3536+
"""
3537+
return [
3538+
ScriptStackFrame(frame) for frame in self._properties.get("stackFrames", [])
3539+
]
3540+
3541+
@property
3542+
def evaluation_kind(self):
3543+
"""str: Indicates the type of child job.
3544+
3545+
Possible values include ``STATEMENT`` and ``EXPRESSION``.
3546+
"""
3547+
return self._properties.get("evaluationKind")

bigquery/tests/unit/test_job.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,37 @@ def test_parent_job_id(self):
276276
job._properties["statistics"] = {"parentJobId": "parent-job-123"}
277277
self.assertEqual(job.parent_job_id, "parent-job-123")
278278

279+
def test_script_statistics(self):
280+
client = _make_client(project=self.PROJECT)
281+
job = self._make_one(self.JOB_ID, client)
282+
283+
self.assertIsNone(job.script_statistics)
284+
job._properties["statistics"] = {
285+
"scriptStatistics": {
286+
"evaluationKind": "EXPRESSION",
287+
"stackFrames": [
288+
{
289+
"startLine": 5,
290+
"startColumn": 29,
291+
"endLine": 9,
292+
"endColumn": 14,
293+
"text": "QUERY TEXT",
294+
}
295+
],
296+
}
297+
}
298+
script_stats = job.script_statistics
299+
self.assertEqual(script_stats.evaluation_kind, "EXPRESSION")
300+
stack_frames = script_stats.stack_frames
301+
self.assertEqual(len(stack_frames), 1)
302+
stack_frame = stack_frames[0]
303+
self.assertIsNone(stack_frame.procedure_id)
304+
self.assertEqual(stack_frame.start_line, 5)
305+
self.assertEqual(stack_frame.start_column, 29)
306+
self.assertEqual(stack_frame.end_line, 9)
307+
self.assertEqual(stack_frame.end_column, 14)
308+
self.assertEqual(stack_frame.text, "QUERY TEXT")
309+
279310
def test_num_child_jobs(self):
280311
client = _make_client(project=self.PROJECT)
281312
job = self._make_one(self.JOB_ID, client)
@@ -5339,6 +5370,92 @@ def test_end(self):
53395370
self.assertEqual(entry.end.strftime(_RFC3339_MICROS), self.END_RFC3339_MICROS)
53405371

53415372

5373+
class TestScriptStackFrame(unittest.TestCase, _Base):
5374+
def _make_one(self, resource):
5375+
from google.cloud.bigquery.job import ScriptStackFrame
5376+
5377+
return ScriptStackFrame(resource)
5378+
5379+
def test_procedure_id(self):
5380+
frame = self._make_one({"procedureId": "some-procedure"})
5381+
self.assertEqual(frame.procedure_id, "some-procedure")
5382+
del frame._properties["procedureId"]
5383+
self.assertIsNone(frame.procedure_id)
5384+
5385+
def test_start_line(self):
5386+
frame = self._make_one({"startLine": 5})
5387+
self.assertEqual(frame.start_line, 5)
5388+
frame._properties["startLine"] = "5"
5389+
self.assertEqual(frame.start_line, 5)
5390+
5391+
def test_start_column(self):
5392+
frame = self._make_one({"startColumn": 29})
5393+
self.assertEqual(frame.start_column, 29)
5394+
frame._properties["startColumn"] = "29"
5395+
self.assertEqual(frame.start_column, 29)
5396+
5397+
def test_end_line(self):
5398+
frame = self._make_one({"endLine": 9})
5399+
self.assertEqual(frame.end_line, 9)
5400+
frame._properties["endLine"] = "9"
5401+
self.assertEqual(frame.end_line, 9)
5402+
5403+
def test_end_column(self):
5404+
frame = self._make_one({"endColumn": 14})
5405+
self.assertEqual(frame.end_column, 14)
5406+
frame._properties["endColumn"] = "14"
5407+
self.assertEqual(frame.end_column, 14)
5408+
5409+
def test_text(self):
5410+
frame = self._make_one({"text": "QUERY TEXT"})
5411+
self.assertEqual(frame.text, "QUERY TEXT")
5412+
5413+
5414+
class TestScriptStatistics(unittest.TestCase, _Base):
5415+
def _make_one(self, resource):
5416+
from google.cloud.bigquery.job import ScriptStatistics
5417+
5418+
return ScriptStatistics(resource)
5419+
5420+
def test_evalutation_kind(self):
5421+
stats = self._make_one({"evaluationKind": "EXPRESSION"})
5422+
self.assertEqual(stats.evaluation_kind, "EXPRESSION")
5423+
self.assertEqual(stats.stack_frames, [])
5424+
5425+
def test_stack_frames(self):
5426+
stats = self._make_one(
5427+
{
5428+
"stackFrames": [
5429+
{
5430+
"procedureId": "some-procedure",
5431+
"startLine": 5,
5432+
"startColumn": 29,
5433+
"endLine": 9,
5434+
"endColumn": 14,
5435+
"text": "QUERY TEXT",
5436+
},
5437+
{},
5438+
]
5439+
}
5440+
)
5441+
stack_frames = stats.stack_frames
5442+
self.assertEqual(len(stack_frames), 2)
5443+
stack_frame = stack_frames[0]
5444+
self.assertEqual(stack_frame.procedure_id, "some-procedure")
5445+
self.assertEqual(stack_frame.start_line, 5)
5446+
self.assertEqual(stack_frame.start_column, 29)
5447+
self.assertEqual(stack_frame.end_line, 9)
5448+
self.assertEqual(stack_frame.end_column, 14)
5449+
self.assertEqual(stack_frame.text, "QUERY TEXT")
5450+
stack_frame = stack_frames[1]
5451+
self.assertIsNone(stack_frame.procedure_id)
5452+
self.assertIsNone(stack_frame.start_line)
5453+
self.assertIsNone(stack_frame.start_column)
5454+
self.assertIsNone(stack_frame.end_line)
5455+
self.assertIsNone(stack_frame.end_column)
5456+
self.assertIsNone(stack_frame.text)
5457+
5458+
53425459
class TestTimelineEntry(unittest.TestCase, _Base):
53435460
ELAPSED_MS = 101
53445461
ACTIVE_UNITS = 50

0 commit comments

Comments
 (0)