Skip to content

Commit b930e46

Browse files
feat: search statistics (#1616)
* experimental tweaks * feat: adds two search statistics classes and property * removes several personal debugging sentinels * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * adds tests * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * cleans up conflict * adds comment * adds some type hints, adds a test for SearchReasons * cleans up some comments * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Update tests/unit/job/test_query_stats.py * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * updated type checks to be isinstance checks per linter * update linting * Update tests/unit/job/test_query_stats.py * Update tests/unit/job/test_query_stats.py * experiments with some tests that are failing * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Fix linting * update package verification approach * update pandas installed version constant * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * remove unused package * set pragma no cover * adds controls to skip testing if pandas exceeds 2.0 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * adds pragma no cover to a simple check * add checks against pandas 2.0 on system test * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * experiments with some tests that are failing * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * resolves merge conflict * resolves merge conflict * resolve conflicts * resolve merge conflicts * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * updates due to faulty confict resolution * adds docstrings to two classes * corrects formatting * Update tests/unit/job/test_query_stats.py * Update tests/unit/job/test_query_stats.py * updates default values and corrects mypy errors * corrects linting * Update google/cloud/bigquery/job/query.py --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 3645e32 commit b930e46

File tree

3 files changed

+153
-1
lines changed

3 files changed

+153
-1
lines changed

google/cloud/bigquery/job/query.py

+62-1
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,59 @@ def from_api_repr(cls, stats: Dict[str, str]) -> "DmlStats":
198198
return cls(*args)
199199

200200

201+
class IndexUnusedReason(typing.NamedTuple):
202+
"""Reason about why no search index was used in the search query (or sub-query).
203+
204+
https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#indexunusedreason
205+
"""
206+
207+
code: Optional[str] = None
208+
"""Specifies the high-level reason for the scenario when no search index was used.
209+
"""
210+
211+
message: Optional[str] = None
212+
"""Free form human-readable reason for the scenario when no search index was used.
213+
"""
214+
215+
baseTable: Optional[TableReference] = None
216+
"""Specifies the base table involved in the reason that no search index was used.
217+
"""
218+
219+
indexName: Optional[str] = None
220+
"""Specifies the name of the unused search index, if available."""
221+
222+
@classmethod
223+
def from_api_repr(cls, reason):
224+
code = reason.get("code")
225+
message = reason.get("message")
226+
baseTable = reason.get("baseTable")
227+
indexName = reason.get("indexName")
228+
229+
return cls(code, message, baseTable, indexName)
230+
231+
232+
class SearchStats(typing.NamedTuple):
233+
"""Statistics related to Search Queries. Populated as part of JobStatistics2.
234+
235+
https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#searchstatistics
236+
"""
237+
238+
mode: Optional[str] = None
239+
"""Indicates the type of search index usage in the entire search query."""
240+
241+
reason: List[IndexUnusedReason] = []
242+
"""Reason about why no search index was used in the search query (or sub-query)"""
243+
244+
@classmethod
245+
def from_api_repr(cls, stats: Dict[str, Any]):
246+
mode = stats.get("indexUsageMode", None)
247+
reason = [
248+
IndexUnusedReason.from_api_repr(r)
249+
for r in stats.get("indexUnusedReasons", [])
250+
]
251+
return cls(mode, reason)
252+
253+
201254
class ScriptOptions:
202255
"""Options controlling the execution of scripts.
203256
@@ -724,7 +777,6 @@ def to_api_repr(self) -> dict:
724777
Dict: A dictionary in the format used by the BigQuery API.
725778
"""
726779
resource = copy.deepcopy(self._properties)
727-
728780
# Query parameters have an addition property associated with them
729781
# to indicate if the query is using named or positional parameters.
730782
query_parameters = resource["query"].get("queryParameters")
@@ -858,6 +910,15 @@ def priority(self):
858910
"""
859911
return self.configuration.priority
860912

913+
@property
914+
def search_stats(self) -> Optional[SearchStats]:
915+
"""Returns a SearchStats object."""
916+
917+
stats = self._job_statistics().get("searchStatistics")
918+
if stats is not None:
919+
return SearchStats.from_api_repr(stats)
920+
return None
921+
861922
@property
862923
def query(self):
863924
"""str: The query text used in this query job.

tests/unit/job/test_query.py

+22
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,28 @@ def test_dml_stats(self):
911911
assert isinstance(job.dml_stats, DmlStats)
912912
assert job.dml_stats.inserted_row_count == 35
913913

914+
def test_search_stats(self):
915+
from google.cloud.bigquery.job.query import SearchStats
916+
917+
client = _make_client(project=self.PROJECT)
918+
job = self._make_one(self.JOB_ID, self.QUERY, client)
919+
assert job.search_stats is None
920+
921+
statistics = job._properties["statistics"] = {}
922+
assert job.search_stats is None
923+
924+
query_stats = statistics["query"] = {}
925+
assert job.search_stats is None
926+
927+
query_stats["searchStatistics"] = {
928+
"indexUsageMode": "INDEX_USAGE_MODE_UNSPECIFIED",
929+
"indexUnusedReasons": [],
930+
}
931+
# job.search_stats is a daisy-chain of calls and gets:
932+
# job.search_stats << job._job_statistics << job._properties
933+
assert isinstance(job.search_stats, SearchStats)
934+
assert job.search_stats.mode == "INDEX_USAGE_MODE_UNSPECIFIED"
935+
914936
def test_result(self):
915937
from google.cloud.bigquery.table import RowIterator
916938

tests/unit/job/test_query_stats.py

+69
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,75 @@ def test_from_api_repr_full_stats(self):
108108
assert result.updated_row_count == 4
109109

110110

111+
class TestSearchStatistics:
112+
@staticmethod
113+
def _get_target_class():
114+
from google.cloud.bigquery.job.query import SearchStats
115+
116+
return SearchStats
117+
118+
def _make_one(self, *args, **kwargs):
119+
return self._get_target_class()(*args, **kwargs)
120+
121+
def test_ctor_defaults(self):
122+
search_stats = self._make_one()
123+
assert search_stats.mode is None
124+
assert search_stats.reason == []
125+
126+
def test_from_api_repr_unspecified(self):
127+
klass = self._get_target_class()
128+
result = klass.from_api_repr(
129+
{"indexUsageMode": "INDEX_USAGE_MODE_UNSPECIFIED", "indexUnusedReasons": []}
130+
)
131+
132+
assert isinstance(result, klass)
133+
assert result.mode == "INDEX_USAGE_MODE_UNSPECIFIED"
134+
assert result.reason == []
135+
136+
137+
class TestIndexUnusedReason:
138+
@staticmethod
139+
def _get_target_class():
140+
from google.cloud.bigquery.job.query import IndexUnusedReason
141+
142+
return IndexUnusedReason
143+
144+
def _make_one(self, *args, **kwargs):
145+
return self._get_target_class()(*args, **kwargs)
146+
147+
def test_ctor_defaults(self):
148+
search_reason = self._make_one()
149+
assert search_reason.code is None
150+
assert search_reason.message is None
151+
assert search_reason.baseTable is None
152+
assert search_reason.indexName is None
153+
154+
def test_from_api_repr_unspecified(self):
155+
klass = self._get_target_class()
156+
result = klass.from_api_repr(
157+
{
158+
"code": "INDEX_CONFIG_NOT_AVAILABLE",
159+
"message": "There is no search index...",
160+
"baseTable": {
161+
"projectId": "bigquery-public-data",
162+
"datasetId": "usa_names",
163+
"tableId": "usa_1910_current",
164+
},
165+
"indexName": None,
166+
}
167+
)
168+
169+
assert isinstance(result, klass)
170+
assert result.code == "INDEX_CONFIG_NOT_AVAILABLE"
171+
assert result.message == "There is no search index..."
172+
assert result.baseTable == {
173+
"projectId": "bigquery-public-data",
174+
"datasetId": "usa_names",
175+
"tableId": "usa_1910_current",
176+
}
177+
assert result.indexName is None
178+
179+
111180
class TestQueryPlanEntryStep(_Base):
112181
KIND = "KIND"
113182
SUBSTEPS = ("SUB1", "SUB2")

0 commit comments

Comments
 (0)