@@ -158,6 +158,7 @@ export const buildQuery = ({
158158 // Detect alias usage in filters (to decide joins/CTEs needed outside)
159159 const filterHasMo = filterString . includes ( 'mo.' )
160160 const filterHasMe = filterString . includes ( 'me.' )
161+ const filterHasM = filterString . includes ( 'm.' ) && ! filterString . match ( / \b m \. i d \b / )
161162 const needsMemberOrgs = includeMemberOrgs || filterHasMo
162163
163164 // If filters pin m.id to a single value or a small IN-list, skip top-N entirely.
@@ -199,61 +200,72 @@ export const buildQuery = ({
199200 ` . trim ( )
200201 }
201202
202- // Decide if we can use the activityCount-optimized path
203+ // Only use activityCount optimization if:
204+ // 1. We have aggregates and are sorting by activityCount
205+ // 2. No filters on member attributes, enrichments, or organizations (only segment/search filters are safe)
206+ // 3. Only basic filters that don't reduce the result set significantly
207+ const hasUnsafeFilters = filterHasMe || filterHasM || filterHasMo
203208 const useActivityCountOptimized =
204- withAggregates && ( ! sortField || sortField === 'activityCount' ) && ! filterHasMe
205- // (we do allow mo.* now, but only outside the CTE; see below)
206- log . info ( `useActivityCountOptimized=${ useActivityCountOptimized } ` )
209+ withAggregates &&
210+ ( ! sortField || sortField === 'activityCount' ) &&
211+ ! hasUnsafeFilters &&
212+ // Only allow if filterString is just basic segment/id filters or empty
213+ ( ! filterString || filterString . trim ( ) === '' || filterString . match ( / ^ \s * 1 \s * = \s * 1 \s * $ / ) )
214+
215+ log . info (
216+ `useActivityCountOptimized=${ useActivityCountOptimized } , hasUnsafeFilters=${ hasUnsafeFilters } ` ,
217+ )
218+
207219 if ( useActivityCountOptimized ) {
208220 const ctes : string [ ] = [ ]
209- if ( needsMemberOrgs ) ctes . push ( buildMemberOrgsCTE ( true ) . trim ( ) )
210221 if ( searchConfig . cte ) ctes . push ( searchConfig . cte . trim ( ) )
211222
212223 const searchJoinForTopMembers = searchConfig . cte
213224 ? `\n INNER JOIN member_search ms ON ms."memberId" = msa."memberId"`
214225 : ''
215226
216- const oversampleMultiplier = 5
217- const prefetch = Math . max ( offset + limit * oversampleMultiplier , offset + limit )
227+ // Fix pagination: fetch enough rows to handle the requested page correctly
228+ const totalNeeded = limit + offset
218229
219230 ctes . push (
220231 `
221- top_members AS (
222- SELECT
223- msa."memberId",
224- msa."activityCount",
225- ROW_NUMBER() OVER (
226- ORDER BY msa."activityCount" ${ direction } NULLS LAST, msa."memberId" ASC
227- ) AS rn
228- FROM "memberSegmentsAgg" msa
229- ${ searchJoinForTopMembers }
230- WHERE msa."segmentId" = $(segmentId)
231- ORDER BY msa."activityCount" ${ direction } NULLS LAST
232- LIMIT ${ prefetch }
233- )
234- ` . trim ( ) ,
232+ top_members AS (
233+ SELECT
234+ msa."memberId",
235+ msa."activityCount"
236+ FROM "memberSegmentsAgg" msa
237+ ${ searchJoinForTopMembers }
238+ WHERE
239+ msa."segmentId" = $(segmentId)
240+ ORDER BY
241+ msa."activityCount" ${ direction } NULLS LAST
242+ LIMIT ${ totalNeeded }
243+ )
244+ ` . trim ( ) ,
235245 )
236246
237247 const withClause = `WITH ${ ctes . join ( ',\n' ) } `
238- const memberOrgsJoin = needsMemberOrgs ? `LEFT JOIN member_orgs mo ON mo."memberId" = m.id` : ''
239248
240249 return `
241- ${ withClause }
242- SELECT ${ fields }
243- FROM top_members tm
244- JOIN members m ON m.id = tm."memberId"
245- INNER JOIN "memberSegmentsAgg" msa
246- ON msa."memberId" = m.id AND msa."segmentId" = $(segmentId)
247- ${ memberOrgsJoin }
248- LEFT JOIN "memberEnrichments" me ON me."memberId" = m.id
249- WHERE (${ filterString } )
250- AND tm.rn > ${ offset }
251- ORDER BY tm.rn ASC
252- LIMIT ${ limit }
253- ` . trim ( )
250+ ${ withClause }
251+ SELECT ${ fields }
252+ FROM top_members tm
253+ JOIN members m
254+ ON m.id = tm."memberId"
255+ INNER JOIN "memberSegmentsAgg" msa
256+ ON msa."memberId" = m.id
257+ AND msa."segmentId" = $(segmentId)
258+ LEFT JOIN "memberEnrichments" me
259+ ON me."memberId" = m.id
260+ WHERE (${ filterString } )
261+ ORDER BY
262+ msa."activityCount" ${ direction } NULLS LAST
263+ LIMIT ${ limit }
264+ OFFSET ${ offset }
265+ ` . trim ( )
254266 }
255267
256- // Fallback path (other sorts / non-aggregate)
268+ // Fallback path (other sorts / non-aggregate / filtered queries )
257269 const baseCtes = [ needsMemberOrgs ? buildMemberOrgsCTE ( true ) : '' , searchConfig . cte ] . filter (
258270 Boolean ,
259271 )
0 commit comments