Repository: googleapis/google-cloud-python

Issue:
Firestore: 'Query.start_at' etc. should accept a list of values for 'document_fields'.
Currently, the "cursor-related" methods ([`Query.start_at`](https://github.com/googleapis/google-cloud-python/blob/4c8184a1023b3c68633c50fdd53fe5136883dd2a/firestore/google/cloud/firestore_v1beta1/query.py#L407-L411), [`Query.start_after`](https://github.com/googleapis/google-cloud-python/blob/4c8184a1023b3c68633c50fdd53fe5136883dd2a/firestore/google/cloud/firestore_v1beta1/query.py#L436-L440), [`Query.end_before`](https://github.com/googleapis/google-cloud-python/blob/4c8184a1023b3c68633c50fdd53fe5136883dd2a/firestore/google/cloud/firestore_v1beta1/query.py#L464-L469), [`Query.end_at`](https://github.com/googleapis/google-cloud-python/blob/4c8184a1023b3c68633c50fdd53fe5136883dd2a/firestore/google/cloud/firestore_v1beta1/query.py#L494-L498)) expect their `document_fields` argument to be either a `DocumentSnapshot` instance or a `dict`, where the fields in either are used to compute the actual values for the cursor, using the query's `order_by` fields.

However, the cross-language conformance tests also expect to be able to pass a raw sequence of values for `document_fields`, which are then used to match the query's `order_by`.  We need to update the docstrings for these methods to indicate that a list is allowed, and fix the [`query._cursor_pb` helper](https://github.com/googleapis/google-cloud-python/blob/4c8184a1023b3c68633c50fdd53fe5136883dd2a/firestore/google/cloud/firestore_v1beta1/query.py#L778-L825) to deal with that case.

The corresponding `Collection` methods also need their docstrings adjusted (they just forward arguments through to a new query object).


Discussion:

Resolving patch:
diff --git a/firestore/google/cloud/firestore_v1beta1/collection.py b/firestore/google/cloud/firestore_v1beta1/collection.py
--- a/firestore/google/cloud/firestore_v1beta1/collection.py
+++ b/firestore/google/cloud/firestore_v1beta1/collection.py
@@ -279,10 +279,10 @@ def start_at(self, document_fields):
 
         Args:
             document_fields (Union[~.firestore_v1beta1.\
-                document.DocumentSnapshot, dict]): Either a document snapshot
-                or a dictionary of fields representing a query results
-                cursor. A cursor is a collection of values that represent a
-                position in a query result set.
+                document.DocumentSnapshot, dict, list, tuple]): a document
+                snapshot or a dictionary/list/tuple of fields representing a
+                query results cursor. A cursor is a collection of values that
+                represent a position in a query result set.
 
         Returns:
             ~.firestore_v1beta1.query.Query: A query with cursor.
@@ -299,10 +299,10 @@ def start_after(self, document_fields):
 
         Args:
             document_fields (Union[~.firestore_v1beta1.\
-                document.DocumentSnapshot, dict]): Either a document snapshot
-                or a dictionary of fields representing a query results
-                cursor. A cursor is a collection of values that represent a
-                position in a query result set.
+                document.DocumentSnapshot, dict, list, tuple]): a document
+                snapshot or a dictionary/list/tuple of fields representing a
+                query results cursor. A cursor is a collection of values that
+                represent a position in a query result set.
 
         Returns:
             ~.firestore_v1beta1.query.Query: A query with cursor.
@@ -319,10 +319,10 @@ def end_before(self, document_fields):
 
         Args:
             document_fields (Union[~.firestore_v1beta1.\
-                document.DocumentSnapshot, dict]): Either a document snapshot
-                or a dictionary of fields representing a query results
-                cursor. A cursor is a collection of values that represent a
-                position in a query result set.
+                document.DocumentSnapshot, dict, list, tuple]): a document
+                snapshot or a dictionary/list/tuple of fields representing a
+                query results cursor. A cursor is a collection of values that
+                represent a position in a query result set.
 
         Returns:
             ~.firestore_v1beta1.query.Query: A query with cursor.
@@ -339,10 +339,10 @@ def end_at(self, document_fields):
 
         Args:
             document_fields (Union[~.firestore_v1beta1.\
-                document.DocumentSnapshot, dict]): Either a document snapshot
-                or a dictionary of fields representing a query results
-                cursor. A cursor is a collection of values that represent a
-                position in a query result set.
+                document.DocumentSnapshot, dict, list, tuple]): a document
+                snapshot or a dictionary/list/tuple of fields representing a
+                query results cursor. A cursor is a collection of values that
+                represent a position in a query result set.
 
         Returns:
             ~.firestore_v1beta1.query.Query: A query with cursor.
diff --git a/firestore/google/cloud/firestore_v1beta1/query.py b/firestore/google/cloud/firestore_v1beta1/query.py
--- a/firestore/google/cloud/firestore_v1beta1/query.py
+++ b/firestore/google/cloud/firestore_v1beta1/query.py
@@ -56,6 +56,8 @@
     'When defining a cursor with one of ``start_at()`` / ``start_after()`` / '
     '``end_before()`` / ``end_at()``, all fields in the cursor must '
     'come from fields set in ``order_by()``.')
+_MISMATCH_CURSOR_W_ORDER_BY = (
+    'The cursor {!r} does not match the order fields {!r}.')
 _EMPTY_DOC_TEMPLATE = (
     'Unexpected server response. All responses other than the first must '
     'contain a document. The response at index {} was\n{}.')
@@ -349,10 +351,10 @@ def _cursor_helper(self, document_fields, before, start):
 
         Args:
             document_fields (Union[~.firestore_v1beta1.\
-                document.DocumentSnapshot, dict]): Either a document snapshot
-                or a dictionary of fields representing a query results
-                cursor. A cursor is a collection of values that represent a
-                position in a query result set.
+                document.DocumentSnapshot, dict, list, tuple]): a document
+                snapshot or a dictionary/list/tuple of fields representing a
+                query results cursor. A cursor is a collection of values that
+                represent a position in a query result set.
             before (bool): Flag indicating if the document in
                 ``document_fields`` should (:data:`False`) or
                 shouldn't (:data:`True`) be included in the result set.
@@ -364,12 +366,13 @@ def _cursor_helper(self, document_fields, before, start):
             a copy of the current query, modified with the newly added
             "start at" cursor.
         """
-        if isinstance(document_fields, dict):
+        if isinstance(document_fields, tuple):
+            document_fields = list(document_fields)
+        elif isinstance(document_fields, document.DocumentSnapshot):
+            document_fields = document_fields.to_dict()
+        else:
             # NOTE: We copy so that the caller can't modify after calling.
             document_fields = copy.deepcopy(document_fields)
-        else:
-            # NOTE: This **assumes** a DocumentSnapshot.
-            document_fields = document_fields.to_dict()
 
         cursor_pair = document_fields, before
         query_kwargs = {
@@ -405,10 +408,10 @@ def start_at(self, document_fields):
 
         Args:
             document_fields (Union[~.firestore_v1beta1.\
-                document.DocumentSnapshot, dict]): Either a document snapshot
-                or a dictionary of fields representing a query results
-                cursor. A cursor is a collection of values that represent a
-                position in a query result set.
+                document.DocumentSnapshot, dict, list, tuple]): a document
+                snapshot or a dictionary/list/tuple of fields representing a
+                query results cursor. A cursor is a collection of values that
+                represent a position in a query result set.
 
         Returns:
             ~.firestore_v1beta1.query.Query: A query with cursor. Acts as
@@ -434,10 +437,10 @@ def start_after(self, document_fields):
 
         Args:
             document_fields (Union[~.firestore_v1beta1.\
-                document.DocumentSnapshot, dict]): Either a document snapshot
-                or a dictionary of fields representing a query results
-                cursor. A cursor is a collection of values that represent a
-                position in a query result set.
+                document.DocumentSnapshot, dict, list, tuple]): a document
+                snapshot or a dictionary/list/tuple of fields representing a
+                query results cursor. A cursor is a collection of values that
+                represent a position in a query result set.
 
         Returns:
             ~.firestore_v1beta1.query.Query: A query with cursor. Acts as
@@ -463,10 +466,10 @@ def end_before(self, document_fields):
 
         Args:
             document_fields (Union[~.firestore_v1beta1.\
-                document.DocumentSnapshot, dict]): Either a document snapshot
-                or a dictionary of fields representing a query results
-                cursor. A cursor is a collection of values that represent a
-                position in a query result set.
+                document.DocumentSnapshot, dict, list, tuple]): a document
+                snapshot or a dictionary/list/tuple of fields representing a
+                query results cursor. A cursor is a collection of values that
+                represent a position in a query result set.
 
         Returns:
             ~.firestore_v1beta1.query.Query: A query with cursor. Acts as
@@ -492,10 +495,10 @@ def end_at(self, document_fields):
 
         Args:
             document_fields (Union[~.firestore_v1beta1.\
-                document.DocumentSnapshot, dict]): Either a document snapshot
-                or a dictionary of fields representing a query results
-                cursor. A cursor is a collection of values that represent a
-                position in a query result set.
+                document.DocumentSnapshot, dict, list, tuple]): a document
+                snapshot or a dictionary/list/tuple of fields representing a
+                query results cursor. A cursor is a collection of values that
+                represent a position in a query result set.
 
         Returns:
             ~.firestore_v1beta1.query.Query: A query with cursor. Acts as
@@ -530,6 +533,38 @@ def _filters_pb(self):
             return query_pb2.StructuredQuery.Filter(
                 composite_filter=composite_filter)
 
+    @staticmethod
+    def _normalize_cursor(cursor, orders):
+        """Helper: convert cursor to a list of values based on orders."""
+        if cursor is None:
+            return
+
+        if not orders:
+            raise ValueError(_NO_ORDERS_FOR_CURSOR)
+
+        document_fields, before = cursor
+
+        order_keys = [order.field.field_path for order in orders]
+
+        if isinstance(document_fields, dict):
+            # Transform to list using orders
+            values = []
+            data = document_fields
+            for order_key in order_keys:
+                try:
+                    values.append(_helpers.get_nested_value(order_key, data))
+                except KeyError:
+                    msg = _MISSING_ORDER_BY.format(order_key, data)
+                    raise ValueError(msg)
+            document_fields = values
+
+        if len(document_fields) != len(orders):
+            msg = _MISMATCH_CURSOR_W_ORDER_BY.format(
+                document_fields, order_keys)
+            raise ValueError(msg)
+
+        return document_fields, before
+
     def _to_protobuf(self):
         """Convert the current query into the equivalent protobuf.
 
@@ -537,6 +572,9 @@ def _to_protobuf(self):
             google.cloud.firestore_v1beta1.types.StructuredQuery: The
             query protobuf.
         """
+        start_at = self._normalize_cursor(self._start_at, self._orders)
+        end_at = self._normalize_cursor(self._end_at, self._orders)
+
         query_kwargs = {
             'select': self._projection,
             'from': [
@@ -546,8 +584,8 @@ def _to_protobuf(self):
             ],
             'where': self._filters_pb(),
             'order_by': self._orders,
-            'start_at': _cursor_pb(self._start_at, self._orders),
-            'end_at': _cursor_pb(self._end_at, self._orders),
+            'start_at': _cursor_pb(start_at),
+            'end_at': _cursor_pb(end_at),
         }
         if self._offset is not None:
             query_kwargs['offset'] = self._offset
@@ -775,54 +813,25 @@ def _filter_pb(field_or_unary):
             'Unexpected filter type', type(field_or_unary), field_or_unary)
 
 
-def _cursor_pb(cursor_pair, orders):
+def _cursor_pb(cursor_pair):
     """Convert a cursor pair to a protobuf.
 
     If ``cursor_pair`` is :data:`None`, just returns :data:`None`.
 
     Args:
-        cursor_pair (Optional[Tuple[dict, bool]]): Two-tuple of
+        cursor_pair (Optional[Tuple[list, bool]]): Two-tuple of
 
-            * a mapping of fields. Any field that is present in this mapping
-              must also be present in ``orders``
+            * a list of field values.
             * a ``before`` flag
 
-        orders (Tuple[google.cloud.proto.firestore.v1beta1.\
-            query_pb2.StructuredQuery.Order, ...]]): The "order by" entries
-            to use for a query. (We use this rather than a list of field path
-            strings just because it is how a query stores calls
-            to ``order_by``.)
-
     Returns:
         Optional[google.cloud.firestore_v1beta1.types.Cursor]: A
         protobuf cursor corresponding to the values.
-
-    Raises:
-        ValueError: If ``cursor_pair`` is not :data:`None`, but there are
-            no ``orders``.
-        ValueError: If one of the field paths in ``orders`` is not contained
-            in the ``data`` (i.e. the first component of ``cursor_pair``).
     """
-    if cursor_pair is None:
-        return None
-
-    if len(orders) == 0:
-        raise ValueError(_NO_ORDERS_FOR_CURSOR)
-
-    data, before = cursor_pair
-    value_pbs = []
-    for order in orders:
-        field_path = order.field.field_path
-        try:
-            value = _helpers.get_nested_value(field_path, data)
-        except KeyError:
-            msg = _MISSING_ORDER_BY.format(field_path, data)
-            raise ValueError(msg)
-
-        value_pb = _helpers.encode_value(value)
-        value_pbs.append(value_pb)
-
-    return query_pb2.Cursor(values=value_pbs, before=before)
+    if cursor_pair is not None:
+        data, before = cursor_pair
+        value_pbs = [_helpers.encode_value(value) for value in data]
+        return query_pb2.Cursor(values=value_pbs, before=before)
 
 
 def _query_response_to_snapshot(response_pb, collection, expected_prefix):


Test patch:
diff --git a/firestore/tests/system.py b/firestore/tests/system.py
--- a/firestore/tests/system.py
+++ b/firestore/tests/system.py
@@ -565,9 +565,8 @@ def test_query_get(client, cleanup):
     assert expected_ab_pairs == ab_pairs2
 
     # 3. Use a start and end cursor.
-    query3 = collection.start_at({'a': num_vals - 2})
-    query3 = query3.order_by('a')
-    query3 = query3.end_before({'a': num_vals - 1})
+    query3 = collection.order_by(
+        'a').start_at({'a': num_vals - 2}).end_before({'a': num_vals - 1})
     values3 = [
         (snapshot.id, snapshot.to_dict())
         for snapshot in query3.get()
diff --git a/firestore/tests/unit/test_query.py b/firestore/tests/unit/test_query.py
--- a/firestore/tests/unit/test_query.py
+++ b/firestore/tests/unit/test_query.py
@@ -248,9 +248,89 @@ def test_offset(self):
         self.assertEqual(query3._offset, offset3)
         self._compare_queries(query2, query3, '_offset')
 
-    def test_start_at(self):
+    @staticmethod
+    def _make_snapshot(values):
         from google.cloud.firestore_v1beta1.document import DocumentSnapshot
 
+        return DocumentSnapshot(None, values, True, None, None, None)
+
+    def test__cursor_helper_w_dict(self):
+        values = {'a': 7, 'b': 'foo'}
+        query1 = self._make_one(mock.sentinel.parent)
+        query2 = query1._cursor_helper(values, True, True)
+
+        self.assertIs(query2._parent, mock.sentinel.parent)
+        self.assertIsNone(query2._projection)
+        self.assertEqual(query2._field_filters, ())
+        self.assertEqual(query2._orders, query1._orders)
+        self.assertIsNone(query2._limit)
+        self.assertIsNone(query2._offset)
+        self.assertIsNone(query2._end_at)
+
+        cursor, before = query2._start_at
+
+        self.assertEqual(cursor, values)
+        self.assertTrue(before)
+
+    def test__cursor_helper_w_tuple(self):
+        values = (7, 'foo')
+        query1 = self._make_one(mock.sentinel.parent)
+        query2 = query1._cursor_helper(values, False, True)
+
+        self.assertIs(query2._parent, mock.sentinel.parent)
+        self.assertIsNone(query2._projection)
+        self.assertEqual(query2._field_filters, ())
+        self.assertEqual(query2._orders, query1._orders)
+        self.assertIsNone(query2._limit)
+        self.assertIsNone(query2._offset)
+        self.assertIsNone(query2._end_at)
+
+        cursor, before = query2._start_at
+
+        self.assertEqual(cursor, list(values))
+        self.assertFalse(before)
+
+    def test__cursor_helper_w_list(self):
+        values = [7, 'foo']
+        query1 = self._make_one(mock.sentinel.parent)
+        query2 = query1._cursor_helper(values, True, False)
+
+        self.assertIs(query2._parent, mock.sentinel.parent)
+        self.assertIsNone(query2._projection)
+        self.assertEqual(query2._field_filters, ())
+        self.assertEqual(query2._orders, query1._orders)
+        self.assertIsNone(query2._limit)
+        self.assertIsNone(query2._offset)
+        self.assertIsNone(query2._start_at)
+
+        cursor, before = query2._end_at
+
+        self.assertEqual(cursor, values)
+        self.assertIsNot(cursor, values)
+        self.assertTrue(before)
+
+    def test__cursor_helper_w_snapshot(self):
+
+        values = {'a': 7, 'b': 'foo'}
+        snapshot = self._make_snapshot(values)
+        query1 = self._make_one(mock.sentinel.parent)
+
+        query2 = query1._cursor_helper(snapshot, False, False)
+
+        self.assertIs(query2._parent, mock.sentinel.parent)
+        self.assertIsNone(query2._projection)
+        self.assertEqual(query2._field_filters, ())
+        self.assertEqual(query2._orders, ())
+        self.assertIsNone(query2._limit)
+        self.assertIsNone(query2._offset)
+        self.assertIsNone(query2._start_at)
+
+        cursor, before = query2._end_at
+
+        self.assertEqual(cursor, values)
+        self.assertFalse(before)
+
+    def test_start_at(self):
         query1 = self._make_one_all_fields(skip_fields=('orders',))
         query2 = query1.order_by('hi')
 
@@ -264,8 +344,7 @@ def test_start_at(self):
         # Make sure it overrides.
         query4 = query3.order_by('bye')
         values5 = {'hi': 'zap', 'bye': 88}
-        document_fields5 = DocumentSnapshot(
-            None, values5, True, None, None, None)
+        document_fields5 = self._make_snapshot(values5)
         query5 = query4.start_at(document_fields5)
         self.assertIsNot(query5, query4)
         self.assertIsInstance(query5, self._get_target_class())
@@ -273,8 +352,6 @@ def test_start_at(self):
         self._compare_queries(query4, query5, '_start_at')
 
     def test_start_after(self):
-        from google.cloud.firestore_v1beta1.document import DocumentSnapshot
-
         query1 = self._make_one_all_fields(skip_fields=('orders',))
         query2 = query1.order_by('down')
 
@@ -288,8 +365,7 @@ def test_start_after(self):
         # Make sure it overrides.
         query4 = query3.order_by('out')
         values5 = {'down': 100.25, 'out': b'\x00\x01'}
-        document_fields5 = DocumentSnapshot(
-            None, values5, True, None, None, None)
+        document_fields5 = self._make_snapshot(values5)
         query5 = query4.start_after(document_fields5)
         self.assertIsNot(query5, query4)
         self.assertIsInstance(query5, self._get_target_class())
@@ -297,8 +373,6 @@ def test_start_after(self):
         self._compare_queries(query4, query5, '_start_at')
 
     def test_end_before(self):
-        from google.cloud.firestore_v1beta1.document import DocumentSnapshot
-
         query1 = self._make_one_all_fields(skip_fields=('orders',))
         query2 = query1.order_by('down')
 
@@ -312,8 +386,7 @@ def test_end_before(self):
         # Make sure it overrides.
         query4 = query3.order_by('out')
         values5 = {'down': 100.25, 'out': b'\x00\x01'}
-        document_fields5 = DocumentSnapshot(
-            None, values5, True, None, None, None)
+        document_fields5 = self._make_snapshot(values5)
         query5 = query4.end_before(document_fields5)
         self.assertIsNot(query5, query4)
         self.assertIsInstance(query5, self._get_target_class())
@@ -321,8 +394,6 @@ def test_end_before(self):
         self._compare_queries(query4, query5, '_end_at')
 
     def test_end_at(self):
-        from google.cloud.firestore_v1beta1.document import DocumentSnapshot
-
         query1 = self._make_one_all_fields(skip_fields=('orders',))
         query2 = query1.order_by('hi')
 
@@ -336,8 +407,7 @@ def test_end_at(self):
         # Make sure it overrides.
         query4 = query3.order_by('bye')
         values5 = {'hi': 'zap', 'bye': 88}
-        document_fields5 = DocumentSnapshot(
-            None, values5, True, None, None, None)
+        document_fields5 = self._make_snapshot(values5)
         query5 = query4.end_at(document_fields5)
         self.assertIsNot(query5, query4)
         self.assertIsInstance(query5, self._get_target_class())
@@ -406,6 +476,49 @@ def test__filters_pb_multi(self):
         )
         self.assertEqual(filter_pb, expected_pb)
 
+    def test__normalize_cursor_none(self):
+        query = self._make_one(mock.sentinel.parent)
+        self.assertIsNone(query._normalize_cursor(None, query._orders))
+
+    def test__normalize_cursor_no_order(self):
+        cursor = ([1], True)
+        query = self._make_one(mock.sentinel.parent)
+
+        with self.assertRaises(ValueError):
+            query._normalize_cursor(cursor, query._orders)
+
+    def test__normalize_cursor_as_list_mismatched_order(self):
+        cursor = ([1, 2], True)
+        query = self._make_one(
+            mock.sentinel.parent).order_by('b', 'ASCENDING')
+
+        with self.assertRaises(ValueError):
+            query._normalize_cursor(cursor, query._orders)
+
+    def test__normalize_cursor_as_dict_mismatched_order(self):
+        cursor = ({'a': 1}, True)
+        query = self._make_one(
+            mock.sentinel.parent).order_by('b', 'ASCENDING')
+
+        with self.assertRaises(ValueError):
+            query._normalize_cursor(cursor, query._orders)
+
+    def test__normalize_cursor_as_list_hit(self):
+        cursor = ([1], True)
+        query = self._make_one(
+            mock.sentinel.parent).order_by('b', 'ASCENDING')
+
+        self.assertEqual(
+            query._normalize_cursor(cursor, query._orders), ([1], True))
+
+    def test__normalize_cursor_as_dict_hit(self):
+        cursor = ({'b': 1}, True)
+        query = self._make_one(
+            mock.sentinel.parent).order_by('b', 'ASCENDING')
+
+        self.assertEqual(
+            query._normalize_cursor(cursor, query._orders), ([1], True))
+
     def test__to_protobuf_all_fields(self):
         from google.protobuf import wrappers_pb2
         from google.cloud.firestore_v1beta1.gapic import enums
@@ -557,11 +670,10 @@ def test__to_protobuf_start_at_only(self):
         from google.cloud.firestore_v1beta1.proto import query_pb2
 
         parent = mock.Mock(id='phish', spec=['id'])
-        query1 = self._make_one(parent)
-        query2 = query1.start_after({'X': {'Y': u'Z'}})
-        query3 = query2.order_by('X.Y')
+        query = self._make_one(
+            parent).order_by('X.Y').start_after({'X': {'Y': u'Z'}})
 
-        structured_query_pb = query3._to_protobuf()
+        structured_query_pb = query._to_protobuf()
         query_kwargs = {
             'from': [
                 query_pb2.StructuredQuery.CollectionSelector(
@@ -590,11 +702,10 @@ def test__to_protobuf_end_at_only(self):
         from google.cloud.firestore_v1beta1.proto import query_pb2
 
         parent = mock.Mock(id='ghoti', spec=['id'])
-        query1 = self._make_one(parent)
-        query2 = query1.end_at({'a': 88})
-        query3 = query2.order_by('a')
+        query = self._make_one(
+            parent).order_by('a').end_at({'a': 88})
 
-        structured_query_pb = query3._to_protobuf()
+        structured_query_pb = query._to_protobuf()
         query_kwargs = {
             'from': [
                 query_pb2.StructuredQuery.CollectionSelector(
@@ -1066,67 +1177,26 @@ def test_bad_type(self):
 class Test__cursor_pb(unittest.TestCase):
 
     @staticmethod
-    def _call_fut(cursor_pair, orders):
+    def _call_fut(cursor_pair):
         from google.cloud.firestore_v1beta1.query import _cursor_pb
 
-        return _cursor_pb(cursor_pair, orders)
+        return _cursor_pb(cursor_pair)
 
     def test_no_pair(self):
-        ret_val = self._call_fut(None, ())
-        self.assertIsNone(ret_val)
-
-    def test_no_orders(self):
-        from google.cloud.firestore_v1beta1.query import _NO_ORDERS_FOR_CURSOR
-
-        cursor_pair = {'a': 'b'}, True
-        with self.assertRaises(ValueError) as exc_info:
-            self._call_fut(cursor_pair, ())
-
-        self.assertEqual(exc_info.exception.args, (_NO_ORDERS_FOR_CURSOR,))
-
-    def test_missing_data(self):
-        from google.cloud.firestore_v1beta1.gapic import enums
-
-        order_pb = _make_order_pb(
-            'a.b', enums.StructuredQuery.Direction.ASCENDING)
-        orders = (order_pb,)
-        data = {}
-        cursor_pair = data, False
-
-        with self.assertRaises(ValueError):
-            self._call_fut(cursor_pair, orders)
+        self.assertIsNone(self._call_fut(None))
 
     def test_success(self):
-        from google.cloud.firestore_v1beta1.gapic import enums
         from google.cloud.firestore_v1beta1.proto import query_pb2
         from google.cloud.firestore_v1beta1 import _helpers
 
-        field_path1 = 'a'
-        field_path2 = 'a.b'
-        field_path3 = 'x'
-        direction1 = enums.StructuredQuery.Direction.DESCENDING
-        direction2 = enums.StructuredQuery.Direction.ASCENDING
-        direction3 = enums.StructuredQuery.Direction.ASCENDING
-        orders = (
-            _make_order_pb(field_path1, direction1),
-            _make_order_pb(field_path2, direction2),
-            _make_order_pb(field_path3, direction3),
-        )
-        data = {
-            'a': {
-                'b': 10,
-                'c': 1.5,
-            },
-            'x': True,
-        }
+        data = [1.5, 10, True]
         cursor_pair = data, True
 
-        cursor_pb = self._call_fut(cursor_pair, orders)
+        cursor_pb = self._call_fut(cursor_pair)
+
         expected_pb = query_pb2.Cursor(
             values=[
-                _helpers.encode_value(data['a']),
-                _helpers.encode_value(data['a']['b']),
-                _helpers.encode_value(data['x']),
+                _helpers.encode_value(value) for value in data
             ],
             before=True,
         )