Fix GraphQL: resume a cursor whose row is gone at its position

Merged
ND
4d ago
+379-12

Fixes #11206

When the row behind an after or before cursor had been deleted or filtered out, the fallback compared the cursor's ISO text with the row's Date and ignored the sort direction. The list then restarted at the first row, or before gave an empty page. The fallback now reads the cursor value on the row's scale and follows the sort direction. Ties on the first key are broken by the id when the id is the next sort key, in either direction. With a longer sort, like channels (isDefault, then createdAt), rows tied on the first key may repeat but are never skipped. A cursor spells a null and an empty text alike, so on a text column the fallback takes the side that repeats rows rather than skipping them. Cursors whose row still exists, the refusals, pageInfo and the cursor format are unchanged.


Summary by cubic

Fixes #11206: pagination now resumes correctly when the row behind an after or before cursor has been deleted or filtered out.

  • The fallback reads the cursor value on the row's scale and follows the sort direction; first-key ties are broken by the id when the id is the next sort key, otherwise the walk resumes at the start or end of the tie group so rows repeat but are never skipped.
  • On text columns an empty cursor value maps to the side that repeats rows; on non-text columns it reads as a null, whether the kind is declared or inferred from the rows' values (covers sortable-only fields like contactBuyers and contactGroups).
  • Cursors whose row still exists, refusals, pageInfo, and the cursor format are unchanged.
  • Adds tests covering after/before around a deleted row across several createdAt/id sort orders, plus null-tie cases for a declared DATE field and a sortable-only numeric field.