Repository: googleapis/google-cloud-python

Issue:
Spanner: IndexError: pop from empty list in _merge_struct
#### Environment details

. OS type and version: Debian 9
. Python version: 3.7
. google-cloud-spanner version:  1.13.0

#### Steps to reproduce

  1. Do any query that brings thousands of results (Issue happens more often after 15k rows). I'm getting this  error inconsistently, sometimes everything works as expected, but some others we get the error. It seems that the chances of getting the error increases if do a query just after executing another one.  

#### Code example

To illustrate take a look at the following code: 

```python
res = []
with db.snapshot(read_timestamp=true_time) as snapshot:
    results = await ioloop.IOLoop.current().run_in_executor(executor, snapshot.execute_sql, sql_f)
                    
    for row in results:
        message = dict(zip(["column1","column2", "column3"], row)) 
        res.append(message) ```

#### Stack trace
```
    Traceback (most recent call last):
      File "/opt/venv/prunes/lib/python3.7/site-packages/tornado/web.py", line 1699, in _execute
        result = await result
      File "/opt/prunes/prunes/handlers/queries.py", line 61, in get
        message = await self.get_one(uuid, true_time, page, db, export, cache, org, None, None, None)
      File "/opt/prunes/prunes/system/queries.py", line 380, in get_one
        page, db, cache, export)
      File "/opt/prunes/prunes/system/queries.py", line 1345, in queries_search
        for row in results:
      File "/opt/venv/prunes/lib/python3.7/site-packages/google/cloud/spanner_v1/streamed.py", line 143, in __iter__
        self._consume_next()
      File "/opt/venv/prunes/lib/python3.7/site-packages/google/cloud/spanner_v1/streamed.py", line 131, in _consume_next
        values[0] = self._merge_chunk(values[0])
      File "/opt/venv/prunes/lib/python3.7/site-packages/google/cloud/spanner_v1/streamed.py", line 92, in _merge_chunk
        merged = _merge_by_type(self._pending_chunk, value, field.type)
      File "/opt/venv/prunes/lib/python3.7/site-packages/google/cloud/spanner_v1/streamed.py", line 305, in _merge_by_type
        return merger(lhs, rhs, type_)
      File "/opt/venv/prunes/lib/python3.7/site-packages/google/cloud/spanner_v1/streamed.py", line 266, in _merge_array
        merged = _merge_by_type(last, first, element_type)
      File "/opt/venv/prunes/lib/python3.7/site-packages/google/cloud/spanner_v1/streamed.py", line 305, in _merge_by_type
        return merger(lhs, rhs, type_)
      File "/opt/venv/prunes/lib/python3.7/site-packages/google/cloud/spanner_v1/streamed.py", line 280, in _merge_struct
        first = rhs.pop(0)
    IndexError: pop from empty list```





Discussion:

Resolving patch:
diff --git a/spanner/google/cloud/spanner_v1/streamed.py b/spanner/google/cloud/spanner_v1/streamed.py
--- a/spanner/google/cloud/spanner_v1/streamed.py
+++ b/spanner/google/cloud/spanner_v1/streamed.py
@@ -276,6 +276,12 @@ def _merge_struct(lhs, rhs, type_):
     """Helper for '_merge_by_type'."""
     fields = type_.struct_type.fields
     lhs, rhs = list(lhs.list_value.values), list(rhs.list_value.values)
+
+    # Sanity check: If either list is empty, short-circuit.
+    # This is effectively a no-op.
+    if not len(lhs) or not len(rhs):
+        return Value(list_value=ListValue(values=(lhs + rhs)))
+
     candidate_type = fields[len(lhs) - 1].type
     first = rhs.pop(0)
     if first.HasField("null_value") or candidate_type.code in _UNMERGEABLE_TYPES:


Test patch:
diff --git a/spanner/tests/unit/test_streamed.py b/spanner/tests/unit/test_streamed.py
--- a/spanner/tests/unit/test_streamed.py
+++ b/spanner/tests/unit/test_streamed.py
@@ -411,6 +411,23 @@ def test__merge_chunk_array_of_struct(self):
         self.assertEqual(merged, expected)
         self.assertIsNone(streamed._pending_chunk)
 
+    def test__merge_chunk_array_of_struct_with_empty(self):
+        iterator = _MockCancellableIterator()
+        streamed = self._make_one(iterator)
+        struct_type = self._make_struct_type([("name", "STRING"), ("age", "INT64")])
+        FIELDS = [self._make_array_field("test", element_type=struct_type)]
+        streamed._metadata = self._make_result_set_metadata(FIELDS)
+        partial = self._make_list_value([u"Phred "])
+        streamed._pending_chunk = self._make_list_value(value_pbs=[partial])
+        rest = self._make_list_value([])
+        chunk = self._make_list_value(value_pbs=[rest])
+
+        merged = streamed._merge_chunk(chunk)
+
+        expected = self._make_list_value(value_pbs=[partial])
+        self.assertEqual(merged, expected)
+        self.assertIsNone(streamed._pending_chunk)
+
     def test__merge_chunk_array_of_struct_unmergeable(self):
         iterator = _MockCancellableIterator()
         streamed = self._make_one(iterator)