@@ -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.
@@ -1280,6 +1285,20 @@ def from_api_repr(cls, resource, client):
12801285 job ._set_properties (resource )
12811286 return job
12821287
1288+ @property
1289+ def query_plan (self ):
1290+ """Return query plan from job statistics, if present.
1291+
1292+ See:
1293+ https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs#statistics.query.queryPlan
1294+
1295+ :rtype: list of :class:`QueryPlanEntry`
1296+ :returns: mappings describing the query plan, or an empty list
1297+ if the query has not yet completed.
1298+ """
1299+ plan_entries = self ._job_statistics ().get ('queryPlan' , ())
1300+ return [QueryPlanEntry .from_api_repr (entry ) for entry in plan_entries ]
1301+
12831302 def query_results (self ):
12841303 """Construct a QueryResults instance, bound to this job.
12851304
@@ -1306,3 +1325,149 @@ def result(self, timeout=None):
13061325 super (QueryJob , self ).result (timeout = timeout )
13071326 # Return a QueryResults instance instead of returning the job.
13081327 return self .query_results ()
1328+
1329+
1330+ class QueryPlanEntryStep (object ):
1331+ """Map a single step in a query plan entry.
1332+
1333+ :type kind: str
1334+ :param kind: step type
1335+
1336+ :type substeps:
1337+ :param substeps: names of substeps
1338+ """
1339+ def __init__ (self , kind , substeps ):
1340+ self .kind = kind
1341+ self .substeps = list (substeps )
1342+
1343+ @classmethod
1344+ def from_api_repr (cls , resource ):
1345+ """Factory: construct instance from the JSON repr.
1346+
1347+ :type resource: dict
1348+ :param resource: JSON representation of the entry
1349+
1350+ :rtype: :class:`QueryPlanEntryStep`
1351+ :return: new instance built from the resource
1352+ """
1353+ return cls (
1354+ kind = resource .get ('kind' ),
1355+ substeps = resource .get ('substeps' , ()),
1356+ )
1357+
1358+ def __eq__ (self , other ):
1359+ if not isinstance (other , self .__class__ ):
1360+ return NotImplemented
1361+ return self .kind == other .kind and self .substeps == other .substeps
1362+
1363+
1364+ class QueryPlanEntry (object ):
1365+ """Map a single entry in a query plan.
1366+
1367+ :type name: str
1368+ :param name: name of the entry
1369+
1370+ :type entry_id: int
1371+ :param entry_id: ID of the entry
1372+
1373+ :type wait_ratio_avg: float
1374+ :param wait_ratio_avg: average wait ratio
1375+
1376+ :type wait_ratio_max: float
1377+ :param wait_ratio_avg: maximum wait ratio
1378+
1379+ :type read_ratio_avg: float
1380+ :param read_ratio_avg: average read ratio
1381+
1382+ :type read_ratio_max: float
1383+ :param read_ratio_avg: maximum read ratio
1384+
1385+ :type copute_ratio_avg: float
1386+ :param copute_ratio_avg: average copute ratio
1387+
1388+ :type copute_ratio_max: float
1389+ :param copute_ratio_avg: maximum copute ratio
1390+
1391+ :type write_ratio_avg: float
1392+ :param write_ratio_avg: average write ratio
1393+
1394+ :type write_ratio_max: float
1395+ :param write_ratio_avg: maximum write ratio
1396+
1397+ :type records_read: int
1398+ :param records_read: number of records read
1399+
1400+ :type records_written: int
1401+ :param records_written: number of records written
1402+
1403+ :type status: str
1404+ :param status: entry status
1405+
1406+ :type steps: List(QueryPlanEntryStep)
1407+ :param steps: steps in the entry
1408+ """
1409+ def __init__ (self ,
1410+ name ,
1411+ entry_id ,
1412+ wait_ratio_avg ,
1413+ wait_ratio_max ,
1414+ read_ratio_avg ,
1415+ read_ratio_max ,
1416+ compute_ratio_avg ,
1417+ compute_ratio_max ,
1418+ write_ratio_avg ,
1419+ write_ratio_max ,
1420+ records_read ,
1421+ records_written ,
1422+ status ,
1423+ steps ):
1424+ self .name = name
1425+ self .entry_id = entry_id
1426+ self .wait_ratio_avg = wait_ratio_avg
1427+ self .wait_ratio_max = wait_ratio_max
1428+ self .read_ratio_avg = read_ratio_avg
1429+ self .read_ratio_max = read_ratio_max
1430+ self .compute_ratio_avg = compute_ratio_avg
1431+ self .compute_ratio_max = compute_ratio_max
1432+ self .write_ratio_avg = write_ratio_avg
1433+ self .write_ratio_max = write_ratio_max
1434+ self .records_read = records_read
1435+ self .records_written = records_written
1436+ self .status = status
1437+ self .steps = steps
1438+
1439+ @classmethod
1440+ def from_api_repr (cls , resource ):
1441+ """Factory: construct instance from the JSON repr.
1442+
1443+ :type resource: dict
1444+ :param resource: JSON representation of the entry
1445+
1446+ :rtype: :class:`QueryPlanEntry`
1447+ :return: new instance built from the resource
1448+ """
1449+ records_read = resource .get ('recordsRead' )
1450+ if records_read is not None :
1451+ records_read = int (records_read )
1452+
1453+ records_written = resource .get ('recordsWritten' )
1454+ if records_written is not None :
1455+ records_written = int (records_written )
1456+
1457+ return cls (
1458+ name = resource .get ('name' ),
1459+ entry_id = resource .get ('id' ),
1460+ wait_ratio_avg = resource .get ('waitRatioAvg' ),
1461+ wait_ratio_max = resource .get ('waitRatioMax' ),
1462+ read_ratio_avg = resource .get ('readRatioAvg' ),
1463+ read_ratio_max = resource .get ('readRatioMax' ),
1464+ compute_ratio_avg = resource .get ('computeRatioAvg' ),
1465+ compute_ratio_max = resource .get ('computeRatioMax' ),
1466+ write_ratio_avg = resource .get ('writeRatioAvg' ),
1467+ write_ratio_max = resource .get ('writeRatioMax' ),
1468+ records_read = records_read ,
1469+ records_written = records_written ,
1470+ status = resource .get ('status' ),
1471+ steps = [QueryPlanEntryStep .from_api_repr (step )
1472+ for step in resource .get ('steps' , ())],
1473+ )
0 commit comments