Repository: googleapis/google-cloud-python

Issue:
BigQuery: expose numDmlAffectedRows in query results
See: https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/query#numDmlAffectedRows

It is marked as experimental. This is because DML statements are currently in beta for BigQuery.

This will be needed to correctly implement [cursor.rowcount](https://www.python.org/dev/peps/pep-0249/#rowcount) for DB-API support https://github.com/GoogleCloudPlatform/google-cloud-python/issues/2434


Discussion:
Hey Tim,
Has this come out of the experimental phase yet?
DML Docs still show beta status.
DML is GA now (for non-partitioned tables), by the way.
https://cloud.google.com/bigquery/docs/reference/standard-sql/data-manipulation-language

Resolving patch:
diff --git a/bigquery/google/cloud/bigquery/query.py b/bigquery/google/cloud/bigquery/query.py
--- a/bigquery/google/cloud/bigquery/query.py
+++ b/bigquery/google/cloud/bigquery/query.py
@@ -226,6 +226,20 @@ def total_bytes_processed(self):
         if total_bytes_processed is not None:
             return int(total_bytes_processed)
 
+    @property
+    def num_dml_affected_rows(self):
+        """Total number of rows affected by a DML query.
+
+        See:
+        https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/query#numDmlAffectedRows
+
+        :rtype: int, or ``NoneType``
+        :returns: Count generated on the server (None until set by the server).
+        """
+        num_dml_affected_rows = self._properties.get('numDmlAffectedRows')
+        if num_dml_affected_rows is not None:
+            return int(num_dml_affected_rows)
+
     @property
     def rows(self):
         """Query results.


Test patch:
diff --git a/bigquery/tests/unit/test_query.py b/bigquery/tests/unit/test_query.py
--- a/bigquery/tests/unit/test_query.py
+++ b/bigquery/tests/unit/test_query.py
@@ -70,6 +70,7 @@ def _makeResource(self, complete=False):
             ]
             resource['pageToken'] = self.TOKEN
             resource['totalBytesProcessed'] = 100000
+            resource['numDmlAffectedRows'] = 123
             resource['cacheHit'] = False
 
         return resource
@@ -124,10 +125,12 @@ def _verifyResourceProperties(self, query, resource):
         self.assertEqual(query.complete, resource.get('jobComplete'))
         self.assertEqual(query.errors, resource.get('errors'))
         self.assertEqual(query.page_token, resource.get('pageToken'))
+
         if 'totalRows' in resource:
             self.assertEqual(query.total_rows, int(resource['totalRows']))
         else:
             self.assertIsNone(query.total_rows)
+
         if 'totalBytesProcessed' in resource:
             self.assertEqual(query.total_bytes_processed,
                              int(resource['totalBytesProcessed']))
@@ -139,6 +142,12 @@ def _verifyResourceProperties(self, query, resource):
         else:
             self.assertIsNone(query.name)
 
+        if 'numDmlAffectedRows' in resource:
+            self.assertEqual(query.num_dml_affected_rows,
+                             int(resource['numDmlAffectedRows']))
+        else:
+            self.assertIsNone(query.num_dml_affected_rows)
+
         self._verify_udf_resources(query, resource)
         self._verifyQueryParameters(query, resource)
         self._verifySchema(query, resource)
@@ -371,6 +380,27 @@ def test_total_bytes_processed_present_string(self):
         query._set_properties(resource)
         self.assertEqual(query.total_bytes_processed, TOTAL_BYTES_PROCESSED)
 
+    def test_num_dml_affected_rows_missing(self):
+        client = _Client(self.PROJECT)
+        query = self._make_one(self.QUERY, client)
+        self.assertIsNone(query.num_dml_affected_rows)
+
+    def test_num_dml_affected_rows_present_integer(self):
+        DML_AFFECTED_ROWS = 123456
+        client = _Client(self.PROJECT)
+        query = self._make_one(self.QUERY, client)
+        resource = {'numDmlAffectedRows': DML_AFFECTED_ROWS}
+        query._set_properties(resource)
+        self.assertEqual(query.num_dml_affected_rows, DML_AFFECTED_ROWS)
+
+    def test_num_dml_affected_rows_present_string(self):
+        DML_AFFECTED_ROWS = 123456
+        client = _Client(self.PROJECT)
+        query = self._make_one(self.QUERY, client)
+        resource = {'numDmlAffectedRows': str(DML_AFFECTED_ROWS)}
+        query._set_properties(resource)
+        self.assertEqual(query.num_dml_affected_rows, DML_AFFECTED_ROWS)
+
     def test_schema(self):
         client = _Client(self.PROJECT)
         query = self._make_one(self.QUERY, client)