Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion services/libs/data-access-layer/src/members/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const QUERY_FILTER_COLUMN_MAP: Map<string, { name: string; queryable?: boolean }
['activityCount', { name: 'msa."activityCount"' }],
['attributes', { name: 'm.attributes' }],
['averageSentiment', { name: 'coalesce(msa."averageSentiment", 0)::decimal' }],
['displayName', { name: 'm."displayName"' }],
['displayName', { name: 'lower(m."displayName")' }],
['id', { name: 'm.id' }],
['identityPlatforms', { name: 'coalesce(msa."activeOn", \'{}\'::text[])' }],
['isBot', { name: `COALESCE((m.attributes -> 'isBot' ->> 'default')::BOOLEAN, FALSE)` }],
Expand Down
46 changes: 36 additions & 10 deletions services/libs/data-access-layer/src/members/queryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,49 +309,75 @@ const buildActivityCountOptimizedQuery = ({
const ctes: string[] = []
if (searchConfig.cte) ctes.push(searchConfig.cte.trim())

const searchJoinForTopMembers = searchConfig.cte
// We must keep msa available in the outer SELECT if "fields" references msa.*
const needsMsaInOuterSelect = /\bmsa\./.test(fields)
const filterNeedsMsa = /\bmsa\./.test(filterString)

const searchJoinForFiltering = searchConfig.cte
? `\n INNER JOIN member_search ms ON ms."memberId" = msa."memberId"`
: ''

const baseNeeded = limit + offset
const oversampleMultiplier = hasNonIdMemberFields ? 10 : 1 // 10x oversampling for m.* filters
const totalNeeded = Math.min(baseNeeded * oversampleMultiplier, 50000) // Cap at 50k

const prefetchLimit = Math.min(totalNeeded * 10, 50000)

const msaJoinForFiltering = filterNeedsMsa
? `\n INNER JOIN "memberSegmentsAgg" msa ON msa."memberId" = m.id AND msa."segmentId" = $(segmentId)`
: ''

ctes.push(
`
top_members AS (
top_msa AS (
SELECT
msa."memberId",
msa."activityCount"
FROM "memberSegmentsAgg" msa
INNER JOIN members m ON m.id = msa."memberId"
${searchJoinForTopMembers}
WHERE
msa."segmentId" = $(segmentId)
AND (${filterString})
ORDER BY
msa."activityCount" ${direction} NULLS LAST
LIMIT ${prefetchLimit}
),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefetch excludes filtered members with low activity counts

The top_msa CTE prefetches members by activity count without applying any filters, then top_members applies filters to this subset. If a user searches or filters for members who have low activity counts, those members won't be in the prefetched set and will be incorrectly excluded from results. The previous logic applied filters before limiting, ensuring all matching members were considered. This can cause queries to return incomplete or missing results when filter criteria don't correlate with activity count.

Fix in Cursor Fix in Web

top_members AS (
SELECT
t."memberId",
t."activityCount"
FROM top_msa t
INNER JOIN members m ON m.id = t."memberId"
${msaJoinForFiltering}
${searchJoinForFiltering}
WHERE
(${filterString})
ORDER BY
t."activityCount" ${direction} NULLS LAST
LIMIT ${totalNeeded}
)
`.trim(),
)

const withClause = `WITH ${ctes.join(',\n')}`

// Outer query is much simpler now - no more filtering needed
const msaOuterJoin = needsMsaInOuterSelect
? `
INNER JOIN "memberSegmentsAgg" msa
ON msa."memberId" = m.id
AND msa."segmentId" = $(segmentId)
`
: ''

return `
${withClause}
SELECT ${fields}
FROM top_members tm
JOIN members m
ON m.id = tm."memberId"
INNER JOIN "memberSegmentsAgg" msa
ON msa."memberId" = m.id
AND msa."segmentId" = $(segmentId)
${msaOuterJoin}
LEFT JOIN "memberEnrichments" me
ON me."memberId" = m.id
ORDER BY
msa."activityCount" ${direction} NULLS LAST
tm."activityCount" ${direction} NULLS LAST
LIMIT ${limit}
OFFSET ${offset}
`.trim()
Expand Down