Repository navigation
Conversation
Rows tied on a non-unique sort column come back in an undefined order, so offset pages overlap and leave gaps: list pages and CSV exports repeated some records and dropped others. buildPaginated now appends missing primary keys to a non-empty sort, in the direction of the last clause so a composite index can still serve it. Empty sorts are left untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (1)
🛟 Help
|
arnaud-moncel
approved these changes
Oct 29, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Rows tied on a non-unique sort column come back in an undefined order, so each
OFFSET/LIMITpage can order them differently: list pages and CSV exports repeated some records and dropped others (customer case: 1,366 exported rows, 1,022 distinct ids, sorted bycreatedDate).buildPaginated(used by list, list-related, csv, csv-related) now appends the missing primary keys to a non-empty sort, in the direction of the last clause so a composite index(createdDate, id)can still be scanned backward.Not in
parseSort: authorization reads it to check which fields the caller sorts on.Evidence
Before:
buildPaginatedwithsort=-createdDate→[{ createdDate, desc }]After:
[{ createdDate, desc }, { id, desc }]; covered for composite keys, primary key already in the sort, non-sortable primary key (skipped), empty sort (untouched), and at route level on the CSV export (sort passed tocollection.list).CSV export run against a local Postgres 16 (
CsvGenerator+buildPaginatedfrom this branch, SQL datasource), 5,000 rows with ~90% sharing the samecreatedAt, exported in chunks of 1,000:-createdAtcreatedAt DESC, id DESC→ 5,000 rows, 5,000 distinctcreatedAtcreatedAt ASC, id ASC→ 5,000 rows, 5,000 distinctTies only reorder between pages once the matching rows exceed twice
offset + limit(Postgres switches from a full quicksort to a bounded heapsort); 1,500 tied rows did not reproduce it.Merge Danger
Door: two-way
Pure query-shape change, revert restores the previous behavior.
Blast Radius: sorted-lists
Only the order of rows tied on the sort changes. Every sorted list/export query gets one more
ORDER BYkey, which can change the query plan on large tables (accepted, no opt-out).Fixes PRD-1866
🤖 Generated with Claude Code