Skip to content

Commit 7482549

Browse files
anmolsahoo25tswast
andauthored
feat: support BI Engine statistics in query job (#1144)
* chore: Add support for accessing BI Engine statistics The REST API returns BiEngineStatistics for a query which denotes if the query was accelerated by BI Engine or not. This commit adds the necessary function to access this information for executed queries. * fix: Removed enums and replaced with string constants * fix: Fixed logic for creating BIEngineStats and added test case * Attempt at mypy fix Co-authored-by: Tim Swast <[email protected]>
1 parent 39ade39 commit 7482549

File tree

3 files changed

+120
-0
lines changed

3 files changed

+120
-0
lines changed

google/cloud/bigquery/job/query.py

+47
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,44 @@ def _to_api_repr_table_defs(value):
121121
return {k: ExternalConfig.to_api_repr(v) for k, v in value.items()}
122122

123123

124+
class BiEngineReason(typing.NamedTuple):
125+
"""Reason for BI Engine acceleration failure
126+
127+
https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#bienginereason
128+
"""
129+
130+
code: str = "CODE_UNSPECIFIED"
131+
132+
reason: str = ""
133+
134+
@classmethod
135+
def from_api_repr(cls, reason: Dict[str, str]) -> "BiEngineReason":
136+
return cls(reason.get("code", "CODE_UNSPECIFIED"), reason.get("message", ""))
137+
138+
139+
class BiEngineStats(typing.NamedTuple):
140+
"""Statistics for a BI Engine query
141+
142+
https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#bienginestatistics
143+
"""
144+
145+
mode: str = "ACCELERATION_MODE_UNSPECIFIED"
146+
""" Specifies which mode of BI Engine acceleration was performed (if any)
147+
"""
148+
149+
reasons: List[BiEngineReason] = []
150+
""" Contains explanatory messages in case of DISABLED / PARTIAL acceleration
151+
"""
152+
153+
@classmethod
154+
def from_api_repr(cls, stats: Dict[str, Any]) -> "BiEngineStats":
155+
mode = stats.get("biEngineMode", "ACCELERATION_MODE_UNSPECIFIED")
156+
reasons = [
157+
BiEngineReason.from_api_repr(r) for r in stats.get("biEngineReasons", [])
158+
]
159+
return cls(mode, reasons)
160+
161+
124162
class DmlStats(typing.NamedTuple):
125163
"""Detailed statistics for DML statements.
126164
@@ -1191,6 +1229,15 @@ def dml_stats(self) -> Optional[DmlStats]:
11911229
else:
11921230
return DmlStats.from_api_repr(stats)
11931231

1232+
@property
1233+
def bi_engine_stats(self) -> Optional[BiEngineStats]:
1234+
stats = self._job_statistics().get("biEngineStatistics")
1235+
1236+
if stats is None:
1237+
return None
1238+
else:
1239+
return BiEngineStats.from_api_repr(stats)
1240+
11941241
def _blocking_poll(self, timeout=None, **kwargs):
11951242
self._done_timeout = timeout
11961243
self._transport_timeout = timeout

tests/unit/job/test_query.py

+17
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,23 @@ def test_estimated_bytes_processed(self):
877877
query_stats["estimatedBytesProcessed"] = str(est_bytes)
878878
self.assertEqual(job.estimated_bytes_processed, est_bytes)
879879

880+
def test_bi_engine_stats(self):
881+
from google.cloud.bigquery.job.query import BiEngineStats
882+
883+
client = _make_client(project=self.PROJECT)
884+
job = self._make_one(self.JOB_ID, self.QUERY, client)
885+
assert job.bi_engine_stats is None
886+
887+
statistics = job._properties["statistics"] = {}
888+
assert job.bi_engine_stats is None
889+
890+
query_stats = statistics["query"] = {}
891+
assert job.bi_engine_stats is None
892+
893+
query_stats["biEngineStatistics"] = {"biEngineMode": "FULL"}
894+
assert isinstance(job.bi_engine_stats, BiEngineStats)
895+
assert job.bi_engine_stats.mode == "FULL"
896+
880897
def test_dml_stats(self):
881898
from google.cloud.bigquery.job.query import DmlStats
882899

tests/unit/job/test_query_stats.py

+56
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,62 @@
1515
from .helpers import _Base
1616

1717

18+
class TestBiEngineStats:
19+
@staticmethod
20+
def _get_target_class():
21+
from google.cloud.bigquery.job.query import BiEngineStats
22+
23+
return BiEngineStats
24+
25+
def _make_one(self, *args, **kw):
26+
return self._get_target_class()(*args, **kw)
27+
28+
def test_ctor_defaults(self):
29+
bi_engine_stats = self._make_one()
30+
assert bi_engine_stats.mode == "ACCELERATION_MODE_UNSPECIFIED"
31+
assert bi_engine_stats.reasons == []
32+
33+
def test_from_api_repr_unspecified(self):
34+
klass = self._get_target_class()
35+
result = klass.from_api_repr({"biEngineMode": "ACCELERATION_MODE_UNSPECIFIED"})
36+
37+
assert isinstance(result, klass)
38+
assert result.mode == "ACCELERATION_MODE_UNSPECIFIED"
39+
assert result.reasons == []
40+
41+
def test_from_api_repr_full(self):
42+
klass = self._get_target_class()
43+
result = klass.from_api_repr({"biEngineMode": "FULL"})
44+
45+
assert isinstance(result, klass)
46+
assert result.mode == "FULL"
47+
assert result.reasons == []
48+
49+
def test_from_api_repr_disabled(self):
50+
klass = self._get_target_class()
51+
result = klass.from_api_repr(
52+
{
53+
"biEngineMode": "DISABLED",
54+
"biEngineReasons": [
55+
{
56+
"code": "OTHER_REASON",
57+
"message": "Unable to support input table xyz due to an internal error.",
58+
}
59+
],
60+
}
61+
)
62+
63+
assert isinstance(result, klass)
64+
assert result.mode == "DISABLED"
65+
66+
reason = result.reasons[0]
67+
assert reason.code == "OTHER_REASON"
68+
assert (
69+
reason.reason
70+
== "Unable to support input table xyz due to an internal error."
71+
)
72+
73+
1874
class TestDmlStats:
1975
@staticmethod
2076
def _get_target_class():

0 commit comments

Comments
 (0)