Skip to content

fix(firestore): fix inclusive cursors in query-to-pipeline conversion - #16822

Open
paulb777 wants to merge 3 commits into
mainfrom
pb-firestore-inclusive-cursor-pipeline
Open

paulb777 wants to merge 3 commits into
mainfrom
pb-firestore-inclusive-cursor-pipeline

Conversation

@paulb777

@paulb777 paulb777 commented Oct 2, 2026

Copy link
Copy Markdown
Member

Problem

db.pipeline().create(from: query) turns query cursors into where stages with
WhereConditionsFromCursor. The function builds the usual lexicographic
predicate, (f0 > v0) OR (f0 == v0 AND f1 > v1) OR ..., with one disjunct per
cursor value. For inclusive bounds (start(at:), end(at:),
start(atDocument:), end(atDocument:)), it was supposed to relax the
comparison in the last disjunct. Instead it relaxed the disjunct at index
orderings.size() - 1. That disjunct is the last one only when the cursor has
exactly one value fewer than the normalized orderings, i.e. one value per
explicit orderBy. In all other cases the pipeline returned the wrong
documents:

  • Document snapshot cursors have a value for every normalized ordering,
    including __name__. The second-to-last disjunct became inclusive, so the key
    component was ignored. Documents that tie with the cursor document on the
    ordered fields but sort before it (for start) or after it (for end) were
    returned.
  • With no explicit orderBy, a key-only cursor never got the inclusive
    comparison, so the boundary document was dropped.
  • With partial cursors (fewer values than explicit orderBys), the inclusive
    comparison was never applied either. start(at:) behaved like
    start(after:) and end(at:) like end(before:).

The JS (whereConditionsFromCursor in pipeline-util.ts) and Android
(Query.whereConditionsFromCursor) SDKs apply the inclusive comparison to the
last cursor component.

Change

  • Make the comparison inclusive in the last disjunct (sub_end == cursors.size()), matching JS and Android.
  • Add PipelineUtilTest cases that convert queries with cursors via
    ToPipelineStages, run the pipeline locally with RunPipeline, and check the
    results. The cases cover document key cursors, key-only cursors, and partial
    cursors, plus a matrix of cursor lengths x inclusive/exclusive x
    start/end that compares the results with Query::Matches.
  • CHANGELOG entry.

Note: the Swift integration test
QueryToPipelineTests.testSupportsStartAtWithDocumentSnapshot is skipped
("sort on name is not working"). It covers this case, and with the old code
its second half also returns document "2". Its JS twin runs unskipped. It can
probably be re-enabled in a follow-up once verified against the Enterprise
backend. This PR does not change it.

Testing

PLATFORM=macOS pod install --project-directory=Firestore/Example
xcodebuild -workspace Firestore/Example/Firestore.xcworkspace -scheme Firestore_Tests_macOS \
  -destination 'platform=macOS' -only-testing:Firestore_Tests_macOS/PipelineUtilTestTests test
xcodebuild -workspace Firestore/Example/Firestore.xcworkspace -scheme Firestore_Tests_macOS \
  -destination 'platform=macOS' test

(Run locally with -derivedDataPath <dir> -jobs 4 added.)

  • Before the fix (only the pipeline_util.cc change reverted): the 4 new tests
    fail with 13 assertion failures, and the 6 existing PipelineUtilTest tests
    pass.
    • ToPipelineStagesInclusiveBoundsWithDocumentKeyCursor: starting at
      [2, k/3] returns k/2..k/5 (expected k/3..k/5). Ending at [2, k/3]
      returns k/1..k/4 (expected k/1..k/3).
    • ToPipelineStagesInclusiveBoundsWithPartialCursor: starting at [2]
      (ordered by a, b) returns only k/5 (expected k/2..k/5). Ending at [2]
      returns only k/1 (expected k/1..k/4).
    • ToPipelineStagesInclusiveBoundsOnDocumentKeyOnly: starting at [k/3]
      returns k/4, k/5. Ending at [k/3] returns k/1, k/2.
    • ToPipelineStagesCursorsMatchQuerySemantics: 7 mismatches against
      Query::Matches.
  • After the fix: PipelineUtilTestTests passes 10/10. The full
    Firestore_Tests_macOS target passes (3120 tests, 0 failures).
  • ./scripts/style.sh on the changed files and git diff --check are clean.

Fixes #16795

Pipeline.create(from:) converts query cursors to where stages with
WhereConditionsFromCursor, which builds one lexicographic disjunct per
cursor value. For inclusive bounds the comparison was relaxed in the
disjunct at index orderings.size() - 1 instead of the last disjunct, so
the result was only correct when the cursor had one value per explicit
orderBy. Document snapshot cursors ignored their document key component
and returned documents that tie on the ordered fields but are on the
wrong side of the cursor document, while key-only and partial cursors
dropped the boundary documents.

Make the comparison on the last cursor component inclusive, matching the
JS and Android SDKs, and add tests that run converted queries and compare
the results with Query::Matches.

Fixes #16795
@gemini-code-assist

Copy link
Copy Markdown
Contributor
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

@paulb777

paulb777 commented Oct 2, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request fixes an issue where pipelines created from a query with an inclusive cursor returned incorrect documents at the cursor boundary by updating the boundary condition in WhereConditionsFromCursor to compare against cursors.size() instead of orderings.size() - 1. Comprehensive unit tests were also added to verify the fix. The review feedback recommends adding a defensive HARD_ASSERT to ensure cursors.size() <= orderings.size(), preventing potential out-of-bounds vector access.

Comment thread Firestore/core/src/core/pipeline_util.cc
…conversion

WhereConditionsFromCursor indexes the orderings by cursor position, so a cursor with more values than orderings would read past the end of the vector. Assert the invariant, as Bound::CompareToDocument does, and add a test. The API layer already rejects such cursors, as suggested in review.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Firestore pipeline conversion drops inclusive cursor boundary values

1 participant