Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support limit to last feature #57

Merged
merged 12 commits into from Jun 26, 2020
45 changes: 37 additions & 8 deletions google/cloud/firestore_v1/collection.py
Expand Up @@ -14,7 +14,6 @@

"""Classes for representing collections for the Google Cloud Firestore API."""
import random
import warnings

import six

Expand Down Expand Up @@ -272,6 +271,24 @@ def limit(self, count):
query = query_mod.Query(self)
return query.limit(count)

def limit_to_last(self, count):
crwilcox marked this conversation as resolved.
Show resolved Hide resolved
"""Create a limited to last query with this collection as parent.

See
IlyaFaer marked this conversation as resolved.
Show resolved Hide resolved
:meth:`~google.cloud.firestore_v1.query.Query.limit_to_last`
for more information on this method.

Args:
count (int): Maximum number of documents to return that
match the query.

Returns:
:class:`~google.cloud.firestore_v1.query.Query`:
A limited to last query.
"""
query = query_mod.Query(self)
return query.limit_to_last(count)

def offset(self, num_to_skip):
"""Skip to an offset in a query with this collection as parent.

Expand Down Expand Up @@ -375,13 +392,25 @@ def end_at(self, document_fields):
return query.end_at(document_fields)

def get(self, transaction=None):
"""Deprecated alias for :meth:`stream`."""
warnings.warn(
"'Collection.get' is deprecated: please use 'Collection.stream' instead.",
DeprecationWarning,
stacklevel=2,
)
return self.stream(transaction=transaction)
"""Read the documents in this collection.
crwilcox marked this conversation as resolved.
Show resolved Hide resolved

This sends a ``RunQuery`` RPC and returns a list of documents
returned in the stream of ``RunQueryResponse`` messages.

Args:
transaction
(Optional[:class:`~google.cloud.firestore_v1.transaction.Transaction`]):
An existing transaction that this query will run in.

If a ``transaction`` is used and it already has write operations
added, this method cannot be used (i.e. read-after-write is not
allowed).

Returns:
list: The documents in this collection that match the query.
"""
query = query_mod.Query(self)
return query.get(transaction=transaction)

def stream(self, transaction=None):
"""Read the documents in this collection.
Expand Down
79 changes: 70 additions & 9 deletions google/cloud/firestore_v1/query.py
Expand Up @@ -20,7 +20,6 @@
"""
import copy
import math
import warnings

from google.protobuf import wrappers_pb2
import six
Expand Down Expand Up @@ -134,6 +133,7 @@ def __init__(
field_filters=(),
orders=(),
limit=None,
limit_to_last=None,
offset=None,
start_at=None,
end_at=None,
Expand All @@ -144,6 +144,7 @@ def __init__(
self._field_filters = field_filters
self._orders = orders
self._limit = limit
self._limit_to_last = limit_to_last
self._offset = offset
self._start_at = start_at
self._end_at = end_at
Expand All @@ -158,6 +159,7 @@ def __eq__(self, other):
and self._field_filters == other._field_filters
and self._orders == other._orders
and self._limit == other._limit
and self._limit_to_last == other._limit_to_last
and self._offset == other._offset
and self._start_at == other._start_at
and self._end_at == other._end_at
Expand Down Expand Up @@ -212,6 +214,7 @@ def select(self, field_paths):
field_filters=self._field_filters,
orders=self._orders,
limit=self._limit,
limit_to_last=self._limit_to_last,
offset=self._offset,
start_at=self._start_at,
end_at=self._end_at,
Expand Down Expand Up @@ -281,6 +284,7 @@ def where(self, field_path, op_string, value):
field_filters=new_filters,
orders=self._orders,
limit=self._limit,
limit_to_last=self._limit_to_last,
offset=self._offset,
start_at=self._start_at,
end_at=self._end_at,
Expand Down Expand Up @@ -333,14 +337,15 @@ def order_by(self, field_path, direction=ASCENDING):
field_filters=self._field_filters,
orders=new_orders,
limit=self._limit,
limit_to_last=self._limit_to_last,
offset=self._offset,
start_at=self._start_at,
end_at=self._end_at,
all_descendants=self._all_descendants,
)

def limit(self, count):
"""Limit a query to return a fixed number of results.
"""Limit a query to return first `count` results.
IlyaFaer marked this conversation as resolved.
Show resolved Hide resolved

If the current query already has a limit set, this will overwrite it.

Expand All @@ -365,6 +370,32 @@ def limit(self, count):
all_descendants=self._all_descendants,
)

def limit_to_last(self, count):
"""Limit a query to return last `count` results.

If the current query already has a limit set, this will overwrite it.

Args:
IlyaFaer marked this conversation as resolved.
Show resolved Hide resolved
count (int): Maximum number of documents to return that match
the query.

Returns:
:class:`~google.cloud.firestore_v1.query.Query`:
A limited query. Acts as a copy of the current query, modified
with the newly added "limit" filter.
"""
return self.__class__(
self._parent,
projection=self._projection,
field_filters=self._field_filters,
orders=self._orders,
limit_to_last=count,
offset=self._offset,
start_at=self._start_at,
end_at=self._end_at,
all_descendants=self._all_descendants,
)

def offset(self, num_to_skip):
"""Skip to an offset in a query.

Expand All @@ -386,6 +417,7 @@ def offset(self, num_to_skip):
field_filters=self._field_filters,
orders=self._orders,
limit=self._limit,
limit_to_last=self._limit_to_last,
offset=num_to_skip,
start_at=self._start_at,
end_at=self._end_at,
Expand Down Expand Up @@ -729,13 +761,37 @@ def _to_protobuf(self):
return query_pb2.StructuredQuery(**query_kwargs)

def get(self, transaction=None):
"""Deprecated alias for :meth:`stream`."""
warnings.warn(
"'Query.get' is deprecated: please use 'Query.stream' instead.",
DeprecationWarning,
stacklevel=2,
)
return self.stream(transaction=transaction)
"""Read the documents in the collection that match this query.

This sends a ``RunQuery`` RPC and returns a list of documents
returned in the stream of ``RunQueryResponse`` messages.

Args:
transaction
(Optional[:class:`~google.cloud.firestore_v1.transaction.Transaction`]):
An existing transaction that this query will run in.

If a ``transaction`` is used and it already has write operations
added, this method cannot be used (i.e. read-after-write is not
allowed).

Returns:
list: The documents in the collection that match this query.
"""
if self._limit_to_last is not None:
self._limit = self._limit_to_last
self._limit_to_last = None

# flip order statements
IlyaFaer marked this conversation as resolved.
Show resolved Hide resolved
for order in self._orders:
order.direction = _enum_from_direction(
self.DESCENDING
if order.direction == self.ASCENDING
else self.ASCENDING
)
crwilcox marked this conversation as resolved.
Show resolved Hide resolved

result = list(self.stream(transaction=transaction))
return list(reversed(result))

def stream(self, transaction=None):
"""Read the documents in the collection that match this query.
Expand Down Expand Up @@ -764,6 +820,11 @@ def stream(self, transaction=None):
:class:`~google.cloud.firestore_v1.document.DocumentSnapshot`:
The next document that fulfills the query.
"""
if self._limit_to_last is not None:
raise ValueError(
"Query results for queries that include limit_to_last() "
"constraints cannot be streamed. Use Query.get() instead."
)
parent_path, expected_prefix = self._parent._parent_info()
response_iterator = self._client._firestore_api.run_query(
parent_path,
Expand Down
35 changes: 17 additions & 18 deletions tests/unit/v1/test_collection.py
Expand Up @@ -371,6 +371,17 @@ def test_limit(self):
self.assertIs(query._parent, collection)
self.assertEqual(query._limit, limit)

def test_limit_to_last(self):
from google.cloud.firestore_v1.query import Query

collection = self._make_one("collection")
LIMIT = 15
query = collection.limit_to_last(LIMIT)

self.assertIsInstance(query, Query)
self.assertIs(query._parent, collection)
self.assertEqual(query._limit_to_last, LIMIT)

def test_offset(self):
from google.cloud.firestore_v1.query import Query

Expand Down Expand Up @@ -484,38 +495,26 @@ def test_list_documents_w_page_size(self):

@mock.patch("google.cloud.firestore_v1.query.Query", autospec=True)
def test_get(self, query_class):
import warnings

collection = self._make_one("collection")
with warnings.catch_warnings(record=True) as warned:
get_response = collection.get()
get_response = collection.get()

query_class.assert_called_once_with(collection)
query_instance = query_class.return_value
self.assertIs(get_response, query_instance.stream.return_value)
query_instance.stream.assert_called_once_with(transaction=None)

# Verify the deprecation
self.assertEqual(len(warned), 1)
self.assertIs(warned[0].category, DeprecationWarning)
self.assertIs(get_response, query_instance.get.return_value)
query_instance.get.assert_called_once_with(transaction=None)

@mock.patch("google.cloud.firestore_v1.query.Query", autospec=True)
def test_get_with_transaction(self, query_class):
import warnings

collection = self._make_one("collection")
transaction = mock.sentinel.txn
with warnings.catch_warnings(record=True) as warned:
get_response = collection.get(transaction=transaction)
get_response = collection.get(transaction=transaction)

query_class.assert_called_once_with(collection)
query_instance = query_class.return_value
self.assertIs(get_response, query_instance.stream.return_value)
query_instance.stream.assert_called_once_with(transaction=transaction)

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

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