Problem
#279 added limit and offset to three of SponsorsService's dashboard lists, but none of them define an order:
// activeBounties
this.bountyRepo.createQueryBuilder('bounty')
.where('bounty.sponsorId = :sponsorId', { sponsorId })
.andWhere('bounty.status NOT IN (:...terminal)', { ... })
.take(options.limit ?? 20)
.skip(options.offset ?? 0)
.getMany();
// activeMilestones
this.milestoneRepo.find({ where: [...], take: ..., skip: ... });
// milestoneProgress
this.milestoneRepo.find({ where: { sponsorId }, take: ..., skip: ... });
Without ORDER BY, Postgres gives no guarantee about row order between two executions of the same query. The plan can change, for example between the new (sponsorId, status) index scan (#345) and a sequential scan, or after concurrent updates and VACUUM. So offset=0 and offset=20 can return the same bounty twice and never return others. A sponsor paging through their dashboard can miss active bounties entirely.
The recentPayments list in the same dashboard() already has .orderBy('payment.createdAt', 'DESC'), so only these three lists are affected.
Suggested fix
Add a deterministic order with a unique tiebreaker to each of the three queries:
activeBounties: .orderBy('bounty.createdAt', 'DESC').addOrderBy('bounty.id', 'DESC')
activeMilestones / milestoneProgress: order: { createdAt: 'DESC', id: 'DESC' }
Extend sponsors.service.spec.ts (#141) to assert the order is applied.
Related: #279, #142, #345, #346.
Problem
#279 added
limitandoffsetto three ofSponsorsService's dashboard lists, but none of them define an order:Without
ORDER BY, Postgres gives no guarantee about row order between two executions of the same query. The plan can change, for example between the new(sponsorId, status)index scan (#345) and a sequential scan, or after concurrent updates andVACUUM. Sooffset=0andoffset=20can return the same bounty twice and never return others. A sponsor paging through their dashboard can miss active bounties entirely.The
recentPaymentslist in the samedashboard()already has.orderBy('payment.createdAt', 'DESC'), so only these three lists are affected.Suggested fix
Add a deterministic order with a unique tiebreaker to each of the three queries:
activeBounties:.orderBy('bounty.createdAt', 'DESC').addOrderBy('bounty.id', 'DESC')activeMilestones/milestoneProgress:order: { createdAt: 'DESC', id: 'DESC' }Extend
sponsors.service.spec.ts(#141) to assert the order is applied.Related: #279, #142, #345, #346.