Skip to content

Commit 44ccf5f

Browse files
committed
refactor: create new build query
1 parent f155430 commit 44ccf5f

File tree

1 file changed

+236
-55
lines changed
  • services/libs/data-access-layer/src/members

1 file changed

+236
-55
lines changed

services/libs/data-access-layer/src/members/base.ts

Lines changed: 236 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,7 @@ export const MEMBER_INSERT_COLUMNS = [
132132
]
133133

134134
const QUERY_FILTER_COLUMN_MAP: Map<string, { name: string; queryable?: boolean }> = new Map([
135-
['activityCount', { name: 'coalesce(msa."activityCount", 0)::integer' }],
136-
['activityCount', { name: 'coalesce(msa."activityCount", 0)::integer' }],
135+
['activityCount', { name: 'msa."activityCount"' }],
137136
['attributes', { name: 'm.attributes' }],
138137
['averageSentiment', { name: 'coalesce(msa."averageSentiment", 0)::decimal' }],
139138
['displayName', { name: 'm."displayName"' }],
@@ -151,26 +150,6 @@ const QUERY_FILTER_COLUMN_MAP: Map<string, { name: string; queryable?: boolean }
151150
['segmentId', { name: 'msa."segmentId"' }],
152151
])
153152

154-
// const QUERY_FILTER_ATTRIBUTE_MAP = ['avatarUrl', 'isBot', 'isTeamMember', 'jobTitle']
155-
156-
const getOrderClause = (orderBy: string, withAggregates: boolean): string => {
157-
const defaultOrder = withAggregates ? 'msa."activityCount" DESC' : 'm."joinedAt" DESC'
158-
159-
if (!orderBy || typeof orderBy !== 'string' || !orderBy.length) {
160-
return defaultOrder
161-
}
162-
163-
const [fieldName, direction = 'DESC'] = orderBy.split('_')
164-
const orderField = QUERY_FILTER_COLUMN_MAP.get(fieldName)?.name
165-
166-
if (!orderField) {
167-
return defaultOrder
168-
}
169-
170-
const orderDirection = ['DESC', 'ASC'].includes(direction) ? direction : 'DESC'
171-
return `${orderField} ${orderDirection}`
172-
}
173-
174153
const buildSearchCTE = (
175154
search: string,
176155
): { cte: string; join: string; params: Record<string, string> } => {
@@ -190,7 +169,8 @@ const buildSearchCTE = (
190169
(mi.verified = true AND mi.type = $(emailType) AND LOWER(mi."value") LIKE $(searchPattern))
191170
OR LOWER(m."displayName") LIKE $(searchPattern)
192171
)
193-
)`,
172+
)
173+
`,
194174
join: `INNER JOIN member_search ms ON ms."memberId" = m.id`,
195175
params: {
196176
emailType: MemberIdentityType.EMAIL,
@@ -210,33 +190,236 @@ const buildMemberOrgsCTE = (includeMemberOrgs: boolean): string => {
210190
FROM "memberOrganizations"
211191
WHERE "deletedAt" IS NULL
212192
GROUP BY "memberId"
213-
)`
193+
)
194+
`
195+
}
196+
197+
type OrderDirection = 'ASC' | 'DESC'
198+
199+
interface SearchConfig {
200+
cte: string
201+
join: string
202+
}
203+
204+
interface BuildQueryArgs {
205+
fields: string
206+
withAggregates: boolean
207+
includeMemberOrgs: boolean
208+
searchConfig: SearchConfig
209+
filterString: string
210+
orderBy?: string // e.g. "activityCount_DESC", "score_ASC", "joinedAt"
211+
orderDirection?: OrderDirection
212+
limit?: number
213+
offset?: number
214+
}
215+
216+
const ORDER_FIELD_MAP: Record<string, string> = {
217+
activityCount: 'msa."activityCount"',
218+
score: 'm."score"',
219+
joinedAt: 'm."joinedAt"',
220+
displayName: 'm."displayName"',
221+
}
222+
223+
const parseOrderBy = (
224+
orderBy: string | undefined,
225+
fallbackDirection: OrderDirection,
226+
): { field?: string; direction: OrderDirection } => {
227+
if (!orderBy || !orderBy.trim()) {
228+
return { field: undefined, direction: fallbackDirection }
229+
}
230+
231+
const [rawField, rawDir] = orderBy.trim().split('_')
232+
const field = rawField?.trim() || undefined
233+
234+
const dir = (rawDir || '').toUpperCase()
235+
const direction: OrderDirection =
236+
dir === 'ASC' || dir === 'DESC'
237+
? (dir as OrderDirection)
238+
: fallbackDirection
239+
240+
return { field, direction }
214241
}
215242

216-
const buildQuery = (
217-
fields: string,
243+
const getOrderClause = (
244+
parsedField: string | undefined,
245+
direction: OrderDirection,
218246
withAggregates: boolean,
219-
includeMemberOrgs: boolean,
220-
searchConfig: { cte: string; join: string },
221-
filterString: string,
222247
): string => {
223-
const ctes = [buildMemberOrgsCTE(includeMemberOrgs), searchConfig.cte].filter(Boolean)
248+
const defaultOrder = withAggregates
249+
? 'msa."activityCount" DESC'
250+
: 'm."joinedAt" DESC'
251+
252+
if (!parsedField) return defaultOrder
253+
254+
const fieldExpr = ORDER_FIELD_MAP[parsedField]
255+
if (!fieldExpr) return defaultOrder
256+
257+
return `${fieldExpr} ${direction}`
258+
}
259+
260+
const buildQuery = ({
261+
fields,
262+
withAggregates,
263+
includeMemberOrgs,
264+
searchConfig,
265+
filterString,
266+
orderBy,
267+
orderDirection,
268+
limit = 20,
269+
offset = 0,
270+
}: BuildQueryArgs): string => {
271+
const fallbackDir: OrderDirection = orderDirection || 'DESC'
272+
const { field: sortField, direction } = parseOrderBy(orderBy, fallbackDir)
273+
274+
// Detect if filters reference extra aliases.
275+
const filterHasMo = filterString.includes('mo.')
276+
const filterHasMe = filterString.includes('me.')
277+
278+
// If filter references mo.*, we must ensure member_orgs is joined.
279+
const needsMemberOrgs = includeMemberOrgs || filterHasMo
280+
281+
// Optimized path is only safe if:
282+
// - withAggregates is true
283+
// - sort is by activityCount (or default)
284+
// - filter does NOT reference mo. or me. (those aliases do not exist in top_members)
285+
const useActivityCountOptimized =
286+
withAggregates &&
287+
!filterHasMo &&
288+
!filterHasMe &&
289+
(!sortField || sortField === 'activityCount')
290+
291+
if (useActivityCountOptimized) {
292+
log.info(`Using optimized activityCount path`)
293+
const ctes: string[] = []
294+
295+
// For optimized path:
296+
// - We MAY include member_orgs CTE only if includeMemberOrgs is true.
297+
// - But filterString is guaranteed not to reference mo/me here.
298+
if (includeMemberOrgs) {
299+
const memberOrgsCTE = buildMemberOrgsCTE(true)
300+
ctes.push(memberOrgsCTE.trim())
301+
}
302+
303+
if (searchConfig.cte) {
304+
ctes.push(searchConfig.cte.trim())
305+
}
306+
307+
const searchJoinInTopMembers = searchConfig.join
308+
? `\n ${searchConfig.join}` // INNER JOIN member_search ms ON ms."memberId" = m.id
309+
: ''
310+
311+
ctes.push(`
312+
top_members AS (
313+
SELECT
314+
msa."memberId"
315+
FROM "memberSegmentsAgg" msa
316+
JOIN members m ON m.id = msa."memberId"
317+
${searchJoinInTopMembers}
318+
WHERE
319+
msa."segmentId" = $(segmentId)
320+
AND (${filterString})
321+
ORDER BY
322+
msa."activityCount" ${direction} NULLS LAST
323+
LIMIT ${limit} OFFSET ${offset}
324+
)
325+
`.trim())
326+
327+
const withClause = `WITH ${ctes.join(',\n')}`
328+
329+
const memberOrgsJoin = includeMemberOrgs
330+
? `LEFT JOIN member_orgs mo ON mo."memberId" = m.id`
331+
: ''
332+
333+
return `
334+
${withClause}
335+
SELECT ${fields}
336+
FROM top_members tm
337+
JOIN members m
338+
ON m.id = tm."memberId"
339+
INNER JOIN "memberSegmentsAgg" msa
340+
ON msa."memberId" = m.id
341+
AND msa."segmentId" = $(segmentId)
342+
${memberOrgsJoin}
343+
LEFT JOIN "memberEnrichments" me
344+
ON me."memberId" = m.id
345+
WHERE (${filterString})
346+
ORDER BY
347+
msa."activityCount" ${direction} NULLS LAST
348+
`.trim()
349+
}
350+
else {
351+
log.info(`Not using optimized activityCount path`)
352+
}
353+
354+
// Fallback path: any case that is not safe/eligible for optimization.
355+
// Here we MUST align joins with what filterString references.
356+
const baseCtes = [
357+
needsMemberOrgs ? buildMemberOrgsCTE(true) : '',
358+
searchConfig.cte,
359+
].filter(Boolean)
224360

225361
const joins = [
226362
withAggregates
227363
? `INNER JOIN "memberSegmentsAgg" msa ON msa."memberId" = m.id AND msa."segmentId" = $(segmentId)`
228364
: '',
229-
includeMemberOrgs ? `LEFT JOIN member_orgs mo ON mo."memberId" = m.id` : '',
365+
needsMemberOrgs
366+
? `LEFT JOIN member_orgs mo ON mo."memberId" = m.id`
367+
: '',
230368
`LEFT JOIN "memberEnrichments" me ON me."memberId" = m.id`,
231369
searchConfig.join,
232370
].filter(Boolean)
233371

372+
const orderClause = getOrderClause(sortField, direction, withAggregates)
373+
234374
return `
235-
${ctes.length > 0 ? `WITH ${ctes.join(',\n')}` : ''}
375+
${baseCtes.length > 0 ? `WITH ${baseCtes.join(',\n')}` : ''}
236376
SELECT ${fields}
237377
FROM members m
238378
${joins.join('\n')}
239379
WHERE (${filterString})
380+
ORDER BY ${orderClause} NULLS LAST
381+
LIMIT ${limit}
382+
OFFSET ${offset}
383+
`.trim()
384+
}
385+
386+
interface BuildCountQueryArgs {
387+
withAggregates: boolean
388+
searchConfig: SearchConfig
389+
filterString: string
390+
includeMemberOrgs?: boolean
391+
}
392+
393+
const buildCountQuery = ({
394+
withAggregates,
395+
searchConfig,
396+
filterString,
397+
includeMemberOrgs = false,
398+
}: BuildCountQueryArgs): string => {
399+
const filterHasMo = filterString.includes('mo.')
400+
const needsMemberOrgs = includeMemberOrgs || filterHasMo
401+
402+
const ctes = [
403+
needsMemberOrgs ? buildMemberOrgsCTE(true) : '',
404+
searchConfig.cte,
405+
].filter(Boolean)
406+
407+
const joins = [
408+
withAggregates
409+
? `INNER JOIN "memberSegmentsAgg" msa ON msa."memberId" = m.id AND msa."segmentId" = $(segmentId)`
410+
: '',
411+
needsMemberOrgs
412+
? `LEFT JOIN member_orgs mo ON mo."memberId" = m.id`
413+
: '',
414+
searchConfig.join,
415+
].filter(Boolean)
416+
417+
return `
418+
${ctes.length > 0 ? `WITH ${ctes.join(',\n')}` : ''}
419+
SELECT COUNT(DISTINCT m.id) AS count
420+
FROM members m
421+
${joins.join('\n')}
422+
WHERE (${filterString})
240423
`.trim()
241424
}
242425

@@ -339,7 +522,7 @@ export async function queryMembersAdvanced(
339522
search = null,
340523
limit = 20,
341524
offset = 0,
342-
orderBy = 'joinedAt_DESC',
525+
orderBy = 'activityCount_DESC',
343526
segmentId = undefined,
344527
countOnly = false,
345528
fields = [...QUERY_FILTER_COLUMN_MAP.keys()],
@@ -361,6 +544,7 @@ export async function queryMembersAdvanced(
361544
attributeSettings = [] as IDbMemberAttributeSetting[],
362545
},
363546
): Promise<PageData<IDbMemberData>> {
547+
364548
const startTime = Date.now()
365549

366550
const withAggregates = !!segmentId
@@ -405,13 +589,12 @@ export async function queryMembersAdvanced(
405589
)
406590

407591
// Build queries
408-
const countQuery = buildQuery(
409-
'COUNT(*) as count',
410-
withAggregates,
411-
include.memberOrganizations,
412-
searchConfig,
413-
filterString,
414-
)
592+
const countQuery = buildCountQuery({
593+
withAggregates,
594+
searchConfig,
595+
filterString,
596+
includeMemberOrgs: include.memberOrganizations,
597+
})
415598

416599
if (countOnly) {
417600
const result = await qx.selectOne(countQuery, params)
@@ -443,21 +626,19 @@ export async function queryMembersAdvanced(
443626
.join(',\n')
444627
log.info(`[PERF] Field preparation took: ${Date.now() - fieldsStartTime}ms`)
445628

446-
const mainQuery = `
447-
${buildQuery(
448-
preparedFields,
449-
withAggregates,
450-
include.memberOrganizations,
451-
searchConfig,
452-
filterString,
453-
)}
454-
ORDER BY ${getOrderClause(orderBy, withAggregates)} NULLS LAST
455-
LIMIT $(limit)
456-
OFFSET $(offset)
457-
`
458-
459-
log.info(`main query: ${formatSql(mainQuery, params)}`)
460-
log.info(`count query: ${formatSql(countQuery, params)}`)
629+
const mainQuery = buildQuery({
630+
fields: preparedFields,
631+
withAggregates, // true when you need memberSegmentsAgg
632+
includeMemberOrgs: include.memberOrganizations,
633+
searchConfig,
634+
filterString,
635+
orderBy, // e.g. 'activityCount' | 'score' | 'joinedAt'
636+
limit,
637+
offset,
638+
})
639+
640+
// log.info(`main query: ${formatSql(mainQuery, params)}`)
641+
// log.info(`count query: ${formatSql(countQuery, params)}`)
461642

462643
// Execute queries in parallel
463644
const mainQueryStartTime = Date.now()

0 commit comments

Comments
 (0)