This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit db03de7f1ae29ed592a6f0541fdd92a93f84a239
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Aug 10 17:31:24 2026 +0200
[v3-3-test] Add backward-walk coverage for cursor-based pagination (#71365)
(#71376)
The keyset pagination helpers are exercised only for forward paging, so a
regression in the backward (previous-page) path — excluding more than the
boundary row, or failing to invert NULL placement when the sort direction
flips — would silently truncate a page walked back into, with nothing to
catch it. This closes that coverage gap.
(cherry picked from commit 54018baa191c47d0d9c93db7fbd211cedcb23b4e)
Co-authored-by: Pierre Jeambrun <[email protected]>
---
.../tests/unit/api_fastapi/common/test_cursors.py | 53 ++++++++++++++++++++++
1 file changed, 53 insertions(+)
diff --git a/airflow-core/tests/unit/api_fastapi/common/test_cursors.py
b/airflow-core/tests/unit/api_fastapi/common/test_cursors.py
index f101efebc9d..bb72908d28a 100644
--- a/airflow-core/tests/unit/api_fastapi/common/test_cursors.py
+++ b/airflow-core/tests/unit/api_fastapi/common/test_cursors.py
@@ -307,3 +307,56 @@ class TestKeysetPaginationNullableColumn:
session.close()
assert collected == [1, 2, 3, 4, 5]
+
+ @staticmethod
+ def _walk_backward(session, model, sort, page_size):
+ """Walk pages from the end to the start: ``_walk_forward`` with the
direction flipped.
+
+ Backward paging is the documented mirror of forward paging (see
``apply_cursor_filter``):
+ reverse the ORDER BY and select rows *before* the cursor. Rows come
back in reverse of
+ the forward order, so the returned list reversed should equal
``_walk_forward``.
+ """
+ collected: list[int] = []
+ token = None
+ for _ in range(50): # guard against an infinite paging loop
+ stmt = sort.to_orm(select(model), reversed=True).limit(page_size)
+ if token is not None:
+ stmt = apply_cursor_filter(stmt, token, sort, "sqlite",
is_backward=True)
+ rows = list(session.scalars(stmt))
+ if not rows:
+ break
+ collected.extend(r.id for r in rows)
+ token = encode_cursor(rows[-1], sort)
+ return collected
+
+ @pytest.mark.parametrize(
+ ("rows", "order_by", "page_size"),
+ [
+ pytest.param([(1, "0"), (2, "0")], ["val"], 1,
id="equal-values-tie-broken-by-pk"),
+ pytest.param([(1, "5"), (2, None)], ["-val"], 1,
id="null-sorts-last-descending"),
+ pytest.param(
+ [(1, None), (2, None), (3, "a"), (4, "b"), (5, "c")], ["val"],
2, id="nulls-first-asc"
+ ),
+ pytest.param(
+ [(1, None), (2, None), (3, "a"), (4, "b"), (5, "c")],
["-val"], 2, id="nulls-last-desc"
+ ),
+ ],
+ )
+ def test_backward_pagination_mirrors_forward(self, rows, order_by,
page_size):
+ """Paging backward yields the same rows as paging forward, only
reversed.
+
+ The backward path must exclude only the boundary row (the last sort
key uses a strict
+ bound; earlier keys stay inclusive) and invert NULL placement;
otherwise a page walked
+ back into is silently truncated.
+ """
+ model, session = self._seed_session(rows)
+ try:
+ sort = SortParam(["val"], model)
+ sort.set_value(order_by)
+ forward = self._walk_forward(session, model, sort, page_size)
+ backward = self._walk_backward(session, model, sort, page_size)
+ finally:
+ session.close()
+
+ assert list(reversed(backward)) == forward
+ assert sorted(forward) == sorted(row[0] for row in rows), "rows
dropped"