Skip to content

Commit 8c75e21

Browse files
Gurov IlyaBenWhiteheadcrwilcox
authored
feat: support limit to last feature (#57)
* feat: implement limit to last feature * simplify test * add more tests and de-deprecate Collection.get() method * fix unit tests * reverse should be used in get() only when limit_to_last set * cosmetic changes * use bool instead of int for _limit_to_last attr * fix comments * fix comments * Apply suggestions from code review Co-authored-by: BenWhitehead <[email protected]> * add notes about mutually exclusivity Co-authored-by: BenWhitehead <[email protected]> Co-authored-by: Christopher Wilcox <[email protected]>
1 parent b9e2ab5 commit 8c75e21

File tree

4 files changed

+229
-46
lines changed

4 files changed

+229
-46
lines changed

google/cloud/firestore_v1/collection.py

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
"""Classes for representing collections for the Google Cloud Firestore API."""
1616
import random
17-
import warnings
1817

1918
import six
2019

@@ -257,6 +256,11 @@ def order_by(self, field_path, **kwargs):
257256
def limit(self, count):
258257
"""Create a limited query with this collection as parent.
259258
259+
.. note::
260+
261+
`limit` and `limit_to_last` are mutually exclusive.
262+
Setting `limit` will drop previously set `limit_to_last`.
263+
260264
See
261265
:meth:`~google.cloud.firestore_v1.query.Query.limit` for
262266
more information on this method.
@@ -272,6 +276,29 @@ def limit(self, count):
272276
query = query_mod.Query(self)
273277
return query.limit(count)
274278

279+
def limit_to_last(self, count):
280+
"""Create a limited to last query with this collection as parent.
281+
282+
.. note::
283+
284+
`limit` and `limit_to_last` are mutually exclusive.
285+
Setting `limit_to_last` will drop previously set `limit`.
286+
287+
See
288+
:meth:`~google.cloud.firestore_v1.query.Query.limit_to_last`
289+
for more information on this method.
290+
291+
Args:
292+
count (int): Maximum number of documents to return that
293+
match the query.
294+
295+
Returns:
296+
:class:`~google.cloud.firestore_v1.query.Query`:
297+
A limited to last query.
298+
"""
299+
query = query_mod.Query(self)
300+
return query.limit_to_last(count)
301+
275302
def offset(self, num_to_skip):
276303
"""Skip to an offset in a query with this collection as parent.
277304
@@ -375,13 +402,25 @@ def end_at(self, document_fields):
375402
return query.end_at(document_fields)
376403

377404
def get(self, transaction=None):
378-
"""Deprecated alias for :meth:`stream`."""
379-
warnings.warn(
380-
"'Collection.get' is deprecated: please use 'Collection.stream' instead.",
381-
DeprecationWarning,
382-
stacklevel=2,
383-
)
384-
return self.stream(transaction=transaction)
405+
"""Read the documents in this collection.
406+
407+
This sends a ``RunQuery`` RPC and returns a list of documents
408+
returned in the stream of ``RunQueryResponse`` messages.
409+
410+
Args:
411+
transaction
412+
(Optional[:class:`~google.cloud.firestore_v1.transaction.Transaction`]):
413+
An existing transaction that this query will run in.
414+
415+
If a ``transaction`` is used and it already has write operations
416+
added, this method cannot be used (i.e. read-after-write is not
417+
allowed).
418+
419+
Returns:
420+
list: The documents in this collection that match the query.
421+
"""
422+
query = query_mod.Query(self)
423+
return query.get(transaction=transaction)
385424

386425
def stream(self, transaction=None):
387426
"""Read the documents in this collection.

google/cloud/firestore_v1/query.py

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
"""
2121
import copy
2222
import math
23-
import warnings
2423

2524
from google.protobuf import wrappers_pb2
2625
import six
@@ -86,6 +85,8 @@ class Query(object):
8685
The "order by" entries to use in the query.
8786
limit (Optional[int]):
8887
The maximum number of documents the query is allowed to return.
88+
limit_to_last (Optional[bool]):
89+
Denotes whether a provided limit is applied to the end of the result set.
8990
offset (Optional[int]):
9091
The number of results to skip.
9192
start_at (Optional[Tuple[dict, bool]]):
@@ -134,6 +135,7 @@ def __init__(
134135
field_filters=(),
135136
orders=(),
136137
limit=None,
138+
limit_to_last=False,
137139
offset=None,
138140
start_at=None,
139141
end_at=None,
@@ -144,6 +146,7 @@ def __init__(
144146
self._field_filters = field_filters
145147
self._orders = orders
146148
self._limit = limit
149+
self._limit_to_last = limit_to_last
147150
self._offset = offset
148151
self._start_at = start_at
149152
self._end_at = end_at
@@ -158,6 +161,7 @@ def __eq__(self, other):
158161
and self._field_filters == other._field_filters
159162
and self._orders == other._orders
160163
and self._limit == other._limit
164+
and self._limit_to_last == other._limit_to_last
161165
and self._offset == other._offset
162166
and self._start_at == other._start_at
163167
and self._end_at == other._end_at
@@ -212,6 +216,7 @@ def select(self, field_paths):
212216
field_filters=self._field_filters,
213217
orders=self._orders,
214218
limit=self._limit,
219+
limit_to_last=self._limit_to_last,
215220
offset=self._offset,
216221
start_at=self._start_at,
217222
end_at=self._end_at,
@@ -281,6 +286,7 @@ def where(self, field_path, op_string, value):
281286
field_filters=new_filters,
282287
orders=self._orders,
283288
limit=self._limit,
289+
limit_to_last=self._limit_to_last,
284290
offset=self._offset,
285291
start_at=self._start_at,
286292
end_at=self._end_at,
@@ -333,16 +339,55 @@ def order_by(self, field_path, direction=ASCENDING):
333339
field_filters=self._field_filters,
334340
orders=new_orders,
335341
limit=self._limit,
342+
limit_to_last=self._limit_to_last,
336343
offset=self._offset,
337344
start_at=self._start_at,
338345
end_at=self._end_at,
339346
all_descendants=self._all_descendants,
340347
)
341348

342349
def limit(self, count):
343-
"""Limit a query to return a fixed number of results.
350+
"""Limit a query to return at most `count` matching results.
344351
345-
If the current query already has a limit set, this will overwrite it.
352+
If the current query already has a `limit` set, this will override it.
353+
354+
.. note::
355+
356+
`limit` and `limit_to_last` are mutually exclusive.
357+
Setting `limit` will drop previously set `limit_to_last`.
358+
359+
Args:
360+
count (int): Maximum number of documents to return that match
361+
the query.
362+
363+
Returns:
364+
:class:`~google.cloud.firestore_v1.query.Query`:
365+
A limited query. Acts as a copy of the current query, modified
366+
with the newly added "limit" filter.
367+
"""
368+
return self.__class__(
369+
self._parent,
370+
projection=self._projection,
371+
field_filters=self._field_filters,
372+
orders=self._orders,
373+
limit=count,
374+
limit_to_last=False,
375+
offset=self._offset,
376+
start_at=self._start_at,
377+
end_at=self._end_at,
378+
all_descendants=self._all_descendants,
379+
)
380+
381+
def limit_to_last(self, count):
382+
"""Limit a query to return the last `count` matching results.
383+
384+
If the current query already has a `limit_to_last`
385+
set, this will override it.
386+
387+
.. note::
388+
389+
`limit` and `limit_to_last` are mutually exclusive.
390+
Setting `limit_to_last` will drop previously set `limit`.
346391
347392
Args:
348393
count (int): Maximum number of documents to return that match
@@ -359,6 +404,7 @@ def limit(self, count):
359404
field_filters=self._field_filters,
360405
orders=self._orders,
361406
limit=count,
407+
limit_to_last=True,
362408
offset=self._offset,
363409
start_at=self._start_at,
364410
end_at=self._end_at,
@@ -386,6 +432,7 @@ def offset(self, num_to_skip):
386432
field_filters=self._field_filters,
387433
orders=self._orders,
388434
limit=self._limit,
435+
limit_to_last=self._limit_to_last,
389436
offset=num_to_skip,
390437
start_at=self._start_at,
391438
end_at=self._end_at,
@@ -729,13 +776,42 @@ def _to_protobuf(self):
729776
return query_pb2.StructuredQuery(**query_kwargs)
730777

731778
def get(self, transaction=None):
732-
"""Deprecated alias for :meth:`stream`."""
733-
warnings.warn(
734-
"'Query.get' is deprecated: please use 'Query.stream' instead.",
735-
DeprecationWarning,
736-
stacklevel=2,
737-
)
738-
return self.stream(transaction=transaction)
779+
"""Read the documents in the collection that match this query.
780+
781+
This sends a ``RunQuery`` RPC and returns a list of documents
782+
returned in the stream of ``RunQueryResponse`` messages.
783+
784+
Args:
785+
transaction
786+
(Optional[:class:`~google.cloud.firestore_v1.transaction.Transaction`]):
787+
An existing transaction that this query will run in.
788+
789+
If a ``transaction`` is used and it already has write operations
790+
added, this method cannot be used (i.e. read-after-write is not
791+
allowed).
792+
793+
Returns:
794+
list: The documents in the collection that match this query.
795+
"""
796+
is_limited_to_last = self._limit_to_last
797+
798+
if self._limit_to_last:
799+
# In order to fetch up to `self._limit` results from the end of the
800+
# query flip the defined ordering on the query to start from the
801+
# end, retrieving up to `self._limit` results from the backend.
802+
for order in self._orders:
803+
order.direction = _enum_from_direction(
804+
self.DESCENDING
805+
if order.direction == self.ASCENDING
806+
else self.ASCENDING
807+
)
808+
self._limit_to_last = False
809+
810+
result = self.stream(transaction=transaction)
811+
if is_limited_to_last:
812+
result = reversed(list(result))
813+
814+
return list(result)
739815

740816
def stream(self, transaction=None):
741817
"""Read the documents in the collection that match this query.
@@ -764,6 +840,11 @@ def stream(self, transaction=None):
764840
:class:`~google.cloud.firestore_v1.document.DocumentSnapshot`:
765841
The next document that fulfills the query.
766842
"""
843+
if self._limit_to_last:
844+
raise ValueError(
845+
"Query results for queries that include limit_to_last() "
846+
"constraints cannot be streamed. Use Query.get() instead."
847+
)
767848
parent_path, expected_prefix = self._parent._parent_info()
768849
response_iterator = self._client._firestore_api.run_query(
769850
parent_path,

tests/unit/v1/test_collection.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,18 @@ def test_limit(self):
371371
self.assertIs(query._parent, collection)
372372
self.assertEqual(query._limit, limit)
373373

374+
def test_limit_to_last(self):
375+
from google.cloud.firestore_v1.query import Query
376+
377+
LIMIT = 15
378+
collection = self._make_one("collection")
379+
query = collection.limit_to_last(LIMIT)
380+
381+
self.assertIsInstance(query, Query)
382+
self.assertIs(query._parent, collection)
383+
self.assertEqual(query._limit, LIMIT)
384+
self.assertTrue(query._limit_to_last)
385+
374386
def test_offset(self):
375387
from google.cloud.firestore_v1.query import Query
376388

@@ -484,38 +496,26 @@ def test_list_documents_w_page_size(self):
484496

485497
@mock.patch("google.cloud.firestore_v1.query.Query", autospec=True)
486498
def test_get(self, query_class):
487-
import warnings
488-
489499
collection = self._make_one("collection")
490-
with warnings.catch_warnings(record=True) as warned:
491-
get_response = collection.get()
500+
get_response = collection.get()
492501

493502
query_class.assert_called_once_with(collection)
494503
query_instance = query_class.return_value
495-
self.assertIs(get_response, query_instance.stream.return_value)
496-
query_instance.stream.assert_called_once_with(transaction=None)
497504

498-
# Verify the deprecation
499-
self.assertEqual(len(warned), 1)
500-
self.assertIs(warned[0].category, DeprecationWarning)
505+
self.assertIs(get_response, query_instance.get.return_value)
506+
query_instance.get.assert_called_once_with(transaction=None)
501507

502508
@mock.patch("google.cloud.firestore_v1.query.Query", autospec=True)
503509
def test_get_with_transaction(self, query_class):
504-
import warnings
505-
506510
collection = self._make_one("collection")
507511
transaction = mock.sentinel.txn
508-
with warnings.catch_warnings(record=True) as warned:
509-
get_response = collection.get(transaction=transaction)
512+
get_response = collection.get(transaction=transaction)
510513

511514
query_class.assert_called_once_with(collection)
512515
query_instance = query_class.return_value
513-
self.assertIs(get_response, query_instance.stream.return_value)
514-
query_instance.stream.assert_called_once_with(transaction=transaction)
515516

516-
# Verify the deprecation
517-
self.assertEqual(len(warned), 1)
518-
self.assertIs(warned[0].category, DeprecationWarning)
517+
self.assertIs(get_response, query_instance.get.return_value)
518+
query_instance.get.assert_called_once_with(transaction=transaction)
519519

520520
@mock.patch("google.cloud.firestore_v1.query.Query", autospec=True)
521521
def test_stream(self, query_class):

0 commit comments

Comments
 (0)