Skip to content

fix(agent): add primary key tiebreaker to paginated sorts - #2049

Open
nbouliol wants to merge 1 commit into
mainfrom
fix/prd-1866-stable-sort-tiebreaker
Open

nbouliol wants to merge 1 commit into
mainfrom
fix/prd-1866-stable-sort-tiebreaker

Conversation

@nbouliol

@nbouliol nbouliol commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Summary

Rows tied on a non-unique sort column come back in an undefined order, so each OFFSET/LIMIT page 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 by createdDate).

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.

 ContextFilterFactory.buildPaginated
-  sort = parseSort(query)
+  sort = parseSort(query)
+  if sort is not empty
+    append sortable primary keys missing from sort, in the last clause's direction
-createdDate          → createdDate DESC, id DESC
-name,createdDate     → name DESC, createdDate ASC, id ASC
createdDate,-id      → unchanged
(no sort)            → unchanged (CSV keeps its own primary-key fallback)

Not in parseSort: authorization reads it to check which fields the caller sorts on.

Evidence

  • Before: buildPaginated with sort=-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 to collection.list).

  • CSV export run against a local Postgres 16 (CsvGenerator + buildPaginated from this branch, SQL datasource), 5,000 rows with ~90% sharing the same createdAt, exported in chunks of 1,000:

    Sort Before After
    -createdAt 5,000 rows, 4,527 distinct ids createdAt DESC, id DESC → 5,000 rows, 5,000 distinct
    createdAt 5,000 rows, 4,600 distinct ids createdAt ASC, id ASC → 5,000 rows, 5,000 distinct

    Ties 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 BY key, which can change the query plan on large tables (accepted, no opt-out).

Fixes PRD-1866

🤖 Generated with Claude Code

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>
@linear-code

linear-code Bot commented Oct 29, 2026

Copy link
Copy Markdown

PRD-1866

@qltysh

qltysh Bot commented Oct 29, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (1)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
packages/agent/src/utils/context-filter-factory.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants