@@ -266,6 +266,11 @@ def ended(self):
266266 if millis is not None :
267267 return _datetime_from_microseconds (millis * 1000.0 )
268268
269+ def _job_statistics (self ):
270+ """Helper for job-type specific statistics-based properties."""
271+ statistics = self ._properties .get ('statistics' , {})
272+ return statistics .get (self ._JOB_TYPE , {})
273+
269274 @property
270275 def error_result (self ):
271276 """Error information about the job as a whole.
@@ -1281,6 +1286,20 @@ def from_api_repr(cls, resource, client):
12811286 job ._set_properties (resource )
12821287 return job
12831288
1289+ @property
1290+ def query_plan (self ):
1291+ """Return query plan from job statistics, if present.
1292+
1293+ See:
1294+ https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs#statistics.query.queryPlan
1295+
1296+ :rtype: list of :class:`QueryPlanEntry`
1297+ :returns: mappings describing the query plan, or an empty list
1298+ if the query has not yet completed.
1299+ """
1300+ plan_entries = self ._job_statistics ().get ('queryPlan' , ())
1301+ return [QueryPlanEntry .from_api_repr (entry ) for entry in plan_entries ]
1302+
12841303 def query_results (self ):
12851304 """Construct a QueryResults instance, bound to this job.
12861305
@@ -1333,3 +1352,149 @@ def result(self, timeout=None):
13331352 super (QueryJob , self ).result (timeout = timeout )
13341353 # Return an iterator instead of returning the job.
13351354 return self .query_results ().fetch_data ()
1355+
1356+
1357+ class QueryPlanEntryStep (object ):
1358+ """Map a single step in a query plan entry.
1359+
1360+ :type kind: str
1361+ :param kind: step type
1362+
1363+ :type substeps:
1364+ :param substeps: names of substeps
1365+ """
1366+ def __init__ (self , kind , substeps ):
1367+ self .kind = kind
1368+ self .substeps = list (substeps )
1369+
1370+ @classmethod
1371+ def from_api_repr (cls , resource ):
1372+ """Factory: construct instance from the JSON repr.
1373+
1374+ :type resource: dict
1375+ :param resource: JSON representation of the entry
1376+
1377+ :rtype: :class:`QueryPlanEntryStep`
1378+ :return: new instance built from the resource
1379+ """
1380+ return cls (
1381+ kind = resource .get ('kind' ),
1382+ substeps = resource .get ('substeps' , ()),
1383+ )
1384+
1385+ def __eq__ (self , other ):
1386+ if not isinstance (other , self .__class__ ):
1387+ return NotImplemented
1388+ return self .kind == other .kind and self .substeps == other .substeps
1389+
1390+
1391+ class QueryPlanEntry (object ):
1392+ """Map a single entry in a query plan.
1393+
1394+ :type name: str
1395+ :param name: name of the entry
1396+
1397+ :type entry_id: int
1398+ :param entry_id: ID of the entry
1399+
1400+ :type wait_ratio_avg: float
1401+ :param wait_ratio_avg: average wait ratio
1402+
1403+ :type wait_ratio_max: float
1404+ :param wait_ratio_avg: maximum wait ratio
1405+
1406+ :type read_ratio_avg: float
1407+ :param read_ratio_avg: average read ratio
1408+
1409+ :type read_ratio_max: float
1410+ :param read_ratio_avg: maximum read ratio
1411+
1412+ :type copute_ratio_avg: float
1413+ :param copute_ratio_avg: average copute ratio
1414+
1415+ :type copute_ratio_max: float
1416+ :param copute_ratio_avg: maximum copute ratio
1417+
1418+ :type write_ratio_avg: float
1419+ :param write_ratio_avg: average write ratio
1420+
1421+ :type write_ratio_max: float
1422+ :param write_ratio_avg: maximum write ratio
1423+
1424+ :type records_read: int
1425+ :param records_read: number of records read
1426+
1427+ :type records_written: int
1428+ :param records_written: number of records written
1429+
1430+ :type status: str
1431+ :param status: entry status
1432+
1433+ :type steps: List(QueryPlanEntryStep)
1434+ :param steps: steps in the entry
1435+ """
1436+ def __init__ (self ,
1437+ name ,
1438+ entry_id ,
1439+ wait_ratio_avg ,
1440+ wait_ratio_max ,
1441+ read_ratio_avg ,
1442+ read_ratio_max ,
1443+ compute_ratio_avg ,
1444+ compute_ratio_max ,
1445+ write_ratio_avg ,
1446+ write_ratio_max ,
1447+ records_read ,
1448+ records_written ,
1449+ status ,
1450+ steps ):
1451+ self .name = name
1452+ self .entry_id = entry_id
1453+ self .wait_ratio_avg = wait_ratio_avg
1454+ self .wait_ratio_max = wait_ratio_max
1455+ self .read_ratio_avg = read_ratio_avg
1456+ self .read_ratio_max = read_ratio_max
1457+ self .compute_ratio_avg = compute_ratio_avg
1458+ self .compute_ratio_max = compute_ratio_max
1459+ self .write_ratio_avg = write_ratio_avg
1460+ self .write_ratio_max = write_ratio_max
1461+ self .records_read = records_read
1462+ self .records_written = records_written
1463+ self .status = status
1464+ self .steps = steps
1465+
1466+ @classmethod
1467+ def from_api_repr (cls , resource ):
1468+ """Factory: construct instance from the JSON repr.
1469+
1470+ :type resource: dict
1471+ :param resource: JSON representation of the entry
1472+
1473+ :rtype: :class:`QueryPlanEntry`
1474+ :return: new instance built from the resource
1475+ """
1476+ records_read = resource .get ('recordsRead' )
1477+ if records_read is not None :
1478+ records_read = int (records_read )
1479+
1480+ records_written = resource .get ('recordsWritten' )
1481+ if records_written is not None :
1482+ records_written = int (records_written )
1483+
1484+ return cls (
1485+ name = resource .get ('name' ),
1486+ entry_id = resource .get ('id' ),
1487+ wait_ratio_avg = resource .get ('waitRatioAvg' ),
1488+ wait_ratio_max = resource .get ('waitRatioMax' ),
1489+ read_ratio_avg = resource .get ('readRatioAvg' ),
1490+ read_ratio_max = resource .get ('readRatioMax' ),
1491+ compute_ratio_avg = resource .get ('computeRatioAvg' ),
1492+ compute_ratio_max = resource .get ('computeRatioMax' ),
1493+ write_ratio_avg = resource .get ('writeRatioAvg' ),
1494+ write_ratio_max = resource .get ('writeRatioMax' ),
1495+ records_read = records_read ,
1496+ records_written = records_written ,
1497+ status = resource .get ('status' ),
1498+ steps = [QueryPlanEntryStep .from_api_repr (step )
1499+ for step in resource .get ('steps' , ())],
1500+ )
0 commit comments