Repository: googleapis/google-cloud-python

Issue:
Firestore: Query's cursor-related methods must not allow transforms
Similar to #6699.

The following conformance tests assert that `start_at`/`start_after`/`end_before`/`end_at` should raise an exception when passed one of  `DELETE_FIELD`, `SERVER_TIMESTAMP`, `ArrayRemove`, or `ArrayUnion`:

- `query-arrayremove-cursor.textproto`
- `query-arrayunion-cursor.textproto`
- `query-del-cursor.textproto`
- `query-st-cursor.textproto`


Discussion:
We can check this and raise inside `_normalize_cursor` (added in #6697).

Resolving patch:
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
@@ -27,6 +27,7 @@
 
 from google.cloud.firestore_v1beta1 import _helpers
 from google.cloud.firestore_v1beta1 import document
+from google.cloud.firestore_v1beta1 import transforms
 from google.cloud.firestore_v1beta1.gapic import enums
 from google.cloud.firestore_v1beta1.proto import query_pb2
 from google.cloud.firestore_v1beta1.order import Order
@@ -46,6 +47,7 @@
 _BAD_OP_NAN_NULL = (
     'Only an equality filter ("==") can be used with None or NaN values')
 _BAD_DIR_STRING = 'Invalid direction {!r}. Must be one of {!r} or {!r}.'
+_INVALID_CURSOR_TRANSFORM = 'Transforms cannot be used as cursor values.'
 _MISSING_ORDER_BY = (
     'The "order by" field path {!r} is not present in the cursor data {!r}. '
     'All fields sent to ``order_by()`` must be present in the fields '
@@ -563,6 +565,12 @@ def _normalize_cursor(cursor, orders):
                 document_fields, order_keys)
             raise ValueError(msg)
 
+        _transform_bases = (transforms.Sentinel, transforms._ValueList)
+        for field in document_fields:
+            if isinstance(field, _transform_bases):
+                msg = _INVALID_CURSOR_TRANSFORM
+                raise ValueError(msg)
+
         return document_fields, before
 
     def _to_protobuf(self):


Test patch:
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
@@ -503,6 +503,46 @@ def test__normalize_cursor_as_dict_mismatched_order(self):
         with self.assertRaises(ValueError):
             query._normalize_cursor(cursor, query._orders)
 
+    def test__normalize_cursor_w_delete(self):
+        from google.cloud.firestore_v1beta1 import DELETE_FIELD
+
+        cursor = ([DELETE_FIELD], 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_w_server_timestamp(self):
+        from google.cloud.firestore_v1beta1 import SERVER_TIMESTAMP
+
+        cursor = ([SERVER_TIMESTAMP], 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_w_array_remove(self):
+        from google.cloud.firestore_v1beta1 import ArrayRemove
+
+        cursor = ([ArrayRemove([1, 3, 5])], 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_w_array_union(self):
+        from google.cloud.firestore_v1beta1 import ArrayUnion
+
+        cursor = ([ArrayUnion([2, 4, 8])], 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(