Skip to content

firestore: AsyncQuery.get() with limit_to_last returns the first N documents in reverse order — the 2023 sync fix (python-firestore#692) was never applied to async_query.py #17767

Description

@phren0logy

Note

This report was researched, reproduced, and written by Claude (an Anthropic AI model) during an automated coding session, and is submitted on behalf of the repository owner's account. The reproduction below was executed for real against google-cloud-firestore 2.25.0 and the Firestore emulator; the source citations were verified against both the installed 2.25.0 package and main at the pinned SHA.

Environment

Summary

AsyncQuery.limit_to_last(N) + .get() returns the first N documents in reverse order instead of the last N in query order. The sync Query behaves correctly.

This is a resurfacing of googleapis/python-firestore#536: the order-direction flip compares a protobuf enum against a string constant, so it never matches and the direction is never flipped. PR googleapis/python-firestore#692 (released in 2.10.0, Feb 2023) fixed the comparison in query.py by comparing order.direction.name — but async_query.py still carries the pre-fix comparison today.

Reproduction

importasyncio, osos.environ.setdefault("FIRESTORE_EMULATOR_HOST", "127.0.0.1:8080")
fromgoogle.cloudimportfirestorefromgoogle.cloud.firestore_v1.async_clientimportAsyncClientasyncdefmain() ->None:
# Async client — WRONGaclient=AsyncClient(project="demo-test")
acol=aclient.collection("ltl-async-probe")
foriinrange(5):
awaitacol.document(f"d{i}").set({"n": i})
adocs=awaitacol.order_by("n").limit_to_last(2).get()
print("async limit_to_last(2).get():", [d.to_dict()["n"] fordinadocs])
# Sync client, same data shape — CORRECTclient=firestore.Client(project="demo-test")
col=client.collection("ltl-sync-probe")
foriinrange(5):
col.document(f"d{i}").set({"n": i})
docs=col.order_by("n").limit_to_last(2).get()
print("sync limit_to_last(2).get():", [d.to_dict()["n"] fordindocs])
asyncio.run(main())

Output (2.25.0):

async limit_to_last(2).get(): [1, 0]
sync limit_to_last(2).get(): [3, 4]

Expected: both print [3, 4] (the last 2 in ascending order, per the limit_to_last documentation).

Root cause

AsyncQuery.get() tries to flip the order direction before streaming, but compares the enum to a string, which is never equal, so every order is (re)set to ASCENDING and the flip is a no-op; the subsequent unconditional reversed(result_list) then reverses the first-N page:

fororderinself._orders:
order.direction=_enum_from_direction(
self.DESCENDING
iforder.direction==self.ASCENDING
elseself.ASCENDING
)
self._limit_to_last=False

fororderinself._orders:
order.direction=_enum_from_direction(
self.DESCENDINGiforder.direction==self.ASCENDING# enum vs "ASCENDING": always Falseelseself.ASCENDING
)

The sync twin was fixed by python-firestore#692 to compare the enum's .name:

fororderinself._orders:
order.direction=_enum_from_direction(
self.DESCENDING
iforder.direction.name==self.ASCENDING
elseself.ASCENDING
)
self._limit_to_last=False

fororderinself._orders:
order.direction=_enum_from_direction(
self.DESCENDINGiforder.direction.name==self.ASCENDING# fixed in #692elseself.ASCENDING
)

A repo-wide grep shows async_query.py:242 is the only remaining instance of the un-fixed comparison.

Suggested fix

Apply the #692 comparison to async_query.py (one token):

- if order.direction == self.ASCENDING+ if order.direction.name == self.ASCENDING

A regression test mirroring whatever coverage #692 added, but on the async client, would prevent the pair from drifting again.

Impact

Any AsyncQuery user of limit_to_last silently gets the wrong window in the wrong order. Notably, google-adk's first-party integrations.firestore.FirestoreSessionService.get_session(config=GetSessionConfig(num_recent_events=N)) routes through this path, so ADK session-history windows are silently incorrect for any session with more than N events (and reversed for all sessions). We hit this in production integration testing and are currently working around it downstream with a DESC + limit + reverse query.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions