Skip to content

Commit 290daec

Browse files
committed
feat: add additional query stats
This PR adds support for incremental query stats.
1 parent ef2740a commit 290daec

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

google/cloud/bigquery/job/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from google.cloud.bigquery.job.query import QueryPlanEntryStep
4040
from google.cloud.bigquery.job.query import ScriptOptions
4141
from google.cloud.bigquery.job.query import TimelineEntry
42+
from google.cloud.bigquery.job.query import IncrementalResultStats
4243
from google.cloud.bigquery.enums import Compression
4344
from google.cloud.bigquery.enums import CreateDisposition
4445
from google.cloud.bigquery.enums import DestinationFormat

google/cloud/bigquery/job/query.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,17 @@ def bi_engine_stats(self) -> Optional[BiEngineStats]:
13391339
else:
13401340
return BiEngineStats.from_api_repr(stats)
13411341

1342+
@property
1343+
def incremental_result_stats(self):
1344+
"""Optional[google.cloud.bigquery.job.IncrementalResultStats]: return information about incremental query results.
1345+
1346+
This feature is not generally available.
1347+
"""
1348+
stats = self._job_statistics().get("incrementalResultStats")
1349+
if stats is not None:
1350+
prop = IncrementalResultStats.from_api_repr(stats)
1351+
return None
1352+
13421353
def _blocking_poll(self, timeout=None, **kwargs):
13431354
self._done_timeout = timeout
13441355
self._transport_timeout = timeout
@@ -2565,3 +2576,63 @@ def slot_millis(self):
25652576
"""Optional[int]: Cumulative slot-milliseconds consumed by
25662577
this query."""
25672578
return _helpers._int_or_none(self._properties.get("totalSlotMs"))
2579+
2580+
2581+
class IncrementalResultStats(object):
2582+
"""IncrementalResultStats provides information about incremental query execution."""
2583+
2584+
def __init__(self):
2585+
self._properties = {}
2586+
2587+
@classmethod
2588+
def from_api_repr(cls, resource):
2589+
"""Factory: construct instance from the JSON repr.
2590+
2591+
Args:
2592+
resource(Dict[str: object]):
2593+
QueryTimelineSample representation returned from API.
2594+
2595+
Returns:
2596+
google.cloud.bigquery.TimelineEntry:
2597+
Timeline sample parsed from ``resource``.
2598+
"""
2599+
entry = cls()
2600+
entry._properties = resource
2601+
return entry
2602+
2603+
@property
2604+
def disabled_reason(self):
2605+
"""Optional[string]: Reason why incremental reasons were not
2606+
written by the query.
2607+
"""
2608+
return _helpers._str_or_none(self._properties.get("disabledReason"))
2609+
2610+
@property
2611+
def result_set_last_replace_time(self):
2612+
"""Optional[datetime]: The time at which the result table's contents
2613+
were completely replaced. May be absent if no results have been written
2614+
or the query has completed."""
2615+
from google.cloud._helpers import _rfc3339_nanos_to_datetime
2616+
2617+
value = self._properties.get("resultSetLastReplaceTime")
2618+
if value:
2619+
try:
2620+
return _rfc3339_nanos_to_datetime(value)
2621+
except ValueError:
2622+
pass
2623+
return None
2624+
2625+
@property
2626+
def result_set_last_modify_time(self):
2627+
"""Optional[datetime]: The time at which the result table's contents
2628+
were completely replaced. May be absent if no results have been written
2629+
or the query has completed."""
2630+
from google.cloud._helpers import _rfc3339_nanos_to_datetime
2631+
2632+
value = self._properties.get("resultSetLastModifyTime")
2633+
if value:
2634+
try:
2635+
return _rfc3339_nanos_to_datetime(value)
2636+
except ValueError:
2637+
pass
2638+
return None

tests/unit/job/test_query_stats.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
from .helpers import _Base
16+
import datetime
1617

1718

1819
class TestBiEngineStats:
@@ -520,3 +521,63 @@ def test_from_api_repr_normal(self):
520521
self.assertEqual(entry.pending_units, self.PENDING_UNITS)
521522
self.assertEqual(entry.completed_units, self.COMPLETED_UNITS)
522523
self.assertEqual(entry.slot_millis, self.SLOT_MILLIS)
524+
525+
526+
class TestIncrementalResultStats:
527+
@staticmethod
528+
def _get_target_class():
529+
from google.cloud.bigquery.job import IncrementalResultStats
530+
531+
return IncrementalResultStats
532+
533+
def _make_one(self, *args, **kw):
534+
return self._get_target_class()(*args, **kw)
535+
536+
def test_ctor_defaults(self):
537+
stats = self._make_one()
538+
assert stats.disabled_reason == None
539+
assert stats.result_set_last_replace_time == None
540+
assert stats.result_set_last_modify_time == None
541+
542+
def test_from_api_repr_partial_stats(self):
543+
klass = self._get_target_class()
544+
stats = klass.from_api_repr({"disabledReason": "FOO"})
545+
546+
assert isinstance(stats, klass)
547+
assert stats.disabled_reason == "FOO"
548+
assert stats.result_set_last_replace_time == None
549+
assert stats.result_set_last_modify_time == None
550+
551+
def test_from_api_repr_full_stats(self):
552+
klass = self._get_target_class()
553+
stats = klass.from_api_repr(
554+
{
555+
"disabledReason": "BAR",
556+
"resultSetLastReplaceTime": "2025-01-02T03:04:05.06Z",
557+
"resultSetLastModifyTime": "2025-02-02T02:02:02.02Z",
558+
}
559+
)
560+
561+
assert isinstance(stats, klass)
562+
assert stats.disabled_reason == "BAR"
563+
assert stats.result_set_last_replace_time == datetime.datetime(
564+
2025, 1, 2, 3, 4, 5, 60000, tzinfo=datetime.timezone.utc
565+
)
566+
assert stats.result_set_last_modify_time == datetime.datetime(
567+
2025, 2, 2, 2, 2, 2, 20000, tzinfo=datetime.timezone.utc
568+
)
569+
570+
def test_from_api_repr_invalid_stats(self):
571+
klass = self._get_target_class()
572+
stats = klass.from_api_repr(
573+
{
574+
"disabledReason": "BAR",
575+
"resultSetLastReplaceTime": "xxx",
576+
"resultSetLastModifyTime": "yyy",
577+
}
578+
)
579+
580+
assert isinstance(stats, klass)
581+
assert stats.disabled_reason == "BAR"
582+
assert stats.result_set_last_replace_time == None
583+
assert stats.result_set_last_modify_time == None

0 commit comments

Comments
 (0)