-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Optimize customers search with FULLTEXT index #3050
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughSwitches customer search endpoints to prefer exact email matches (when applicable) and otherwise use sanitized full-text search on email and name; adds a projectId+email index and a full-text index on (email,name); and makes several UI tweaks to filters, combobox behavior, and search input presentation. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant API as Customers API
participant DB as Prisma/DB
Client->>API: GET /api/customers?search=...&country=...&linkId=...
activate API
alt search present
alt search contains "@"
API->>API: where = { email: search }
else
API->>API: where = { email: { search: sanitizeFullTextSearch(search) }, name: { search: sanitizeFullTextSearch(search) } }
end
else no search
API->>API: where = {}
end
API->>API: spread/add top-level filters: country, linkId
API->>DB: Query (uses projectId+email index and fulltext on email,name)
activate DB
DB-->>API: Results
deactivate DB
API-->>Client: Response
deactivate API
Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~45 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touchesโ Failed checks (1 warning)
โ Passed checks (2 passed)
โจ Finishing touches
๐งช Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (2)
apps/web/app/(ee)/api/customers/route.ts(2 hunks)packages/prisma/schema/customer.prisma(1 hunks)
๐งฐ Additional context used
๐ง Learnings (1)
๐ Learning: 2025-09-24T16:13:00.387Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: packages/prisma/schema/partner.prisma:151-153
Timestamp: 2025-09-24T16:13:00.387Z
Learning: In the Dub codebase, Prisma schemas use single-column indexes without brackets (e.g., `@index(partnerId)`) and multi-column indexes with brackets (e.g., `@index([programId, partnerId])`). This syntax pattern is consistently used throughout their schema files and works correctly with their Prisma version.
Applied to files:
packages/prisma/schema/customer.prismaapps/web/app/(ee)/api/customers/route.ts
๐งฌ Code graph analysis (1)
apps/web/app/(ee)/api/customers/route.ts (1)
packages/prisma/index.ts (1)
sanitizeFullTextSearch(19-22)
โฐ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
๐ Additional comments (3)
apps/web/app/(ee)/api/customers/route.ts (1)
15-15: LGTM!The import of
sanitizeFullTextSearchis correctly added and used in the search logic below.packages/prisma/schema/customer.prisma (2)
28-28: LGTM!The composite index on
[projectId, email]efficiently supports exact email lookups when search contains "@", which is the common case for email-based searches.
34-34: Full-text index is correctly configured for the MySQL database.The production database is configured as MySQL (in
packages/prisma/schema/schema.prisma), which natively supports full-text indexes since version 5.6+. The@@fulltext([email, name, externalId])index on the Customer model is properly defined for MySQL and will function as intended for full-text search operations.
|
@coderabbitai full review |
โ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
โป๏ธ Duplicate comments (1)
apps/web/app/(ee)/api/customers/route.ts (1)
69-74: Full-text search still requires both fields to matchWe're still intersecting the full-text predicates (
name) at the same level, so Prisma issues anANDbetween them. That means any search term that only matches one field now returns zero rowsโthe exact bug flagged in the earlier review. Please wrap those predicates in anOR(and reuse a sanitized token) so that matching either field returns results.- : search - ? search.includes("@") - ? { email: search } - : { - email: { search: sanitizeFullTextSearch(search) }, - name: { search: sanitizeFullTextSearch(search) }, - } - : {}), + : search + ? search.includes("@") + ? { email: search } + : (() => { + const sanitized = sanitizeFullTextSearch(search); + return sanitized + ? { + OR: [ + { email: { search: sanitized } }, + { name: { search: sanitized } }, + ], + } + : {}; + })() + : {}),
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (10)
apps/web/app/(ee)/api/customers/count/route.ts(3 hunks)apps/web/app/(ee)/api/customers/route.ts(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/use-payout-filters.tsx(0 hunks)apps/web/lib/swr/use-customers-count.ts(1 hunks)apps/web/ui/customers/customer-table/customer-table.tsx(1 hunks)apps/web/ui/customers/customer-table/use-customer-filters.tsx(3 hunks)packages/prisma/schema/customer.prisma(1 hunks)packages/ui/src/combobox/index.tsx(3 hunks)packages/ui/src/filter/filter-list.tsx(3 hunks)packages/ui/src/filter/filter-select.tsx(3 hunks)
๐ค Files with no reviewable changes (1)
- apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/use-payout-filters.tsx
๐ง Files skipped from review as they are similar to previous changes (1)
- packages/prisma/schema/customer.prisma
๐งฐ Additional context used
๐ง Learnings (5)
๐ Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.
Applied to files:
apps/web/app/(ee)/api/customers/count/route.ts
๐ Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.
Applied to files:
packages/ui/src/filter/filter-list.tsx
๐ Learning: 2025-09-24T16:13:00.387Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: packages/prisma/schema/partner.prisma:151-153
Timestamp: 2025-09-24T16:13:00.387Z
Learning: In the Dub codebase, Prisma schemas use single-column indexes without brackets (e.g., `@index(partnerId)`) and multi-column indexes with brackets (e.g., `@index([programId, partnerId])`). This syntax pattern is consistently used throughout their schema files and works correctly with their Prisma version.
Applied to files:
apps/web/app/(ee)/api/customers/route.ts
๐ Learning: 2025-08-26T15:05:55.081Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/swr/use-bounty.ts:11-16
Timestamp: 2025-08-26T15:05:55.081Z
Learning: In the Dub codebase, workspace authentication and route structures prevent endless loading states when workspaceId or similar route parameters are missing, so gating SWR loading states on parameter availability is often unnecessary.
Applied to files:
apps/web/ui/customers/customer-table/use-customer-filters.tsx
๐ Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/ui/customers/customer-table/use-customer-filters.tsx
๐งฌ Code graph analysis (4)
apps/web/app/(ee)/api/customers/count/route.ts (1)
packages/prisma/index.ts (1)
sanitizeFullTextSearch(19-22)
packages/ui/src/filter/filter-list.tsx (2)
packages/ui/src/combobox/index.tsx (2)
ComboboxOption(29-37)Combobox(86-369)packages/ui/src/icons/index.tsx (1)
Icon(80-80)
packages/ui/src/combobox/index.tsx (1)
packages/ui/src/icons/index.tsx (1)
Icon(80-80)
apps/web/app/(ee)/api/customers/route.ts (1)
packages/prisma/index.ts (1)
sanitizeFullTextSearch(19-22)
โฐ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
โป๏ธ Duplicate comments (3)
packages/ui/src/filter/filter-list.tsx (1)
296-314: Preserve original typed filter values in callbacks.
ComboboxOption.valueis always a string (line 180:value: String(opt.value)), butFilterOption["value"]can bestring | number | boolean. PassingnewOption.valueorsearchValuedirectly toonSelectdowngrades numeric/boolean filter values to strings, breaking downstream consumers that expect the original typed value.Apply this diff to look up and preserve the original
FilterOption["value"]:setSelected={(newOption: ComboboxOption | null) => { if (newOption && newOption.value !== String(value)) { - // Remove the current value and add the new one - onRemove(filterKey, value); - onSelect(filterKey, newOption.value); + const matchedFilterOption = filter.options?.find((opt) => + typeof opt.value === "string" && typeof newOption.value === "string" + ? opt.value.toLowerCase() === newOption.value.toLowerCase() + : String(opt.value) === newOption.value, + ); + const typedValue = (matchedFilterOption?.value ?? newOption.value) as FilterOption["value"]; + + onRemove(filterKey, value); + onSelect(filterKey, typedValue); } }} options={options} onSearchChange={setSearch} onCreate={ hasEmptyOptions && onSelect ? async (searchValue: string) => { - // Select the search value as a new option + const matchedFilterOption = filter.options?.find((opt) => + typeof opt.value === "string" + ? opt.value.toLowerCase() === searchValue.toLowerCase() + : String(opt.value) === searchValue, + ); + const typedValue = (matchedFilterOption?.value ?? searchValue) as FilterOption["value"]; + onRemove(filterKey, value); - onSelect(filterKey, searchValue); + onSelect(filterKey, typedValue); return true; } : undefined }apps/web/app/(ee)/api/customers/count/route.ts (1)
18-25: Use OR logic for full-text search and guard empty sanitized queries.The current implementation has two critical issues:
Unintended AND logic: Applying
namefilters in the same object forces Prisma to AND the predicates, requiring both columns to match. This dramatically narrows results compared to the intended "email OR name" behavior.Unguarded empty query: When
sanitizeFullTextSearchstrips all characters (e.g., user enters"+++"), Prisma forwards an empty string to Postgres, causingsyntax error in tsquery: "".Apply this diff to fix both issues:
+ const sanitizedSearch = + search && !search.includes("@") ? sanitizeFullTextSearch(search) : ""; + const commonWhere: Prisma.CustomerWhereInput = { projectId: workspace.id, ...(email ? { email } : externalId ? { externalId } : search ? search.includes("@") ? { email: search } - : { - email: { search: sanitizeFullTextSearch(search) }, - name: { search: sanitizeFullTextSearch(search) }, - } + : sanitizedSearch + ? { + OR: [ + { email: { search: sanitizedSearch } }, + { name: { search: sanitizedSearch } }, + ], + } + : { + OR: [ + { + email: { + contains: search, + mode: Prisma.QueryMode.insensitive, + }, + }, + { + name: { + contains: search, + mode: Prisma.QueryMode.insensitive, + }, + }, + ], + } : {}),apps/web/app/(ee)/api/customers/route.ts (1)
69-75: Restore OR semantics for multi-field full-text searchPrisma combines sibling conditions with AND, so this branch still requires both
nameto match the term. Any record missing one of those fields (common for customers without a name) now drops out of search results โ the same regression flagged earlier. Please wrap thesesearchfilters in anORso a match in either column qualifies, and keep the exact-email short circuit.- : search - ? search.includes("@") - ? { email: search } - : { - email: { search: sanitizeFullTextSearch(search) }, - name: { search: sanitizeFullTextSearch(search) }, - } + : search + ? search.includes("@") + ? { + OR: [ + { email: search }, + { email: { search: sanitizeFullTextSearch(search) } }, + { name: { search: sanitizeFullTextSearch(search) } }, + ], + } + : { + OR: [ + { email: { search: sanitizeFullTextSearch(search) } }, + { name: { search: sanitizeFullTextSearch(search) } }, + ], + } : {}), </blockquote></details> </blockquote></details> <details> <summary>๐งน Nitpick comments (1)</summary><blockquote> <details> <summary>packages/prisma/schema/customer.prisma (1)</summary><blockquote> `28-34`: **Consider adding index for projectId + externalId queries.** Removing `@@index([projectId, email, externalId, name])` may degrade performance for queries filtering by `projectId` and `externalId` together. While the `@@unique([projectId, externalId])` constraint exists, it's not optimized for filtering operations. If queries commonly filter by both `projectId` and `externalId`, consider adding: ```diff @@unique([projectId, externalId]) @@unique([projectConnectId, externalId]) @@index([projectId, email]) + @@index([projectId, externalId]) @@index([projectId, createdAt])
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (10)
apps/web/app/(ee)/api/customers/count/route.ts(3 hunks)apps/web/app/(ee)/api/customers/route.ts(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/use-payout-filters.tsx(0 hunks)apps/web/lib/swr/use-customers-count.ts(1 hunks)apps/web/ui/customers/customer-table/customer-table.tsx(1 hunks)apps/web/ui/customers/customer-table/use-customer-filters.tsx(3 hunks)packages/prisma/schema/customer.prisma(1 hunks)packages/ui/src/combobox/index.tsx(3 hunks)packages/ui/src/filter/filter-list.tsx(3 hunks)packages/ui/src/filter/filter-select.tsx(3 hunks)
๐ค Files with no reviewable changes (1)
- apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/use-payout-filters.tsx
๐งฐ Additional context used
๐ง Learnings (6)
๐ Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.
Applied to files:
packages/ui/src/filter/filter-list.tsx
๐ Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.
Applied to files:
packages/ui/src/filter/filter-list.tsxpackages/ui/src/combobox/index.tsx
๐ Learning: 2025-09-24T16:13:00.387Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: packages/prisma/schema/partner.prisma:151-153
Timestamp: 2025-09-24T16:13:00.387Z
Learning: In the Dub codebase, Prisma schemas use single-column indexes without brackets (e.g., `@index(partnerId)`) and multi-column indexes with brackets (e.g., `@index([programId, partnerId])`). This syntax pattern is consistently used throughout their schema files and works correctly with their Prisma version.
Applied to files:
apps/web/app/(ee)/api/customers/route.tspackages/prisma/schema/customer.prisma
๐ Learning: 2025-08-26T15:05:55.081Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/swr/use-bounty.ts:11-16
Timestamp: 2025-08-26T15:05:55.081Z
Learning: In the Dub codebase, workspace authentication and route structures prevent endless loading states when workspaceId or similar route parameters are missing, so gating SWR loading states on parameter availability is often unnecessary.
Applied to files:
apps/web/ui/customers/customer-table/use-customer-filters.tsx
๐ Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/ui/customers/customer-table/use-customer-filters.tsx
๐ Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.
Applied to files:
apps/web/app/(ee)/api/customers/count/route.ts
๐ Additional comments (5)
apps/web/lib/swr/use-customers-count.ts (1)
24-24: LGTM!Adding
externalIdto the query parameters properly extends the customer count API to support external ID filtering, which aligns with the PR's broader customer search enhancements.packages/ui/src/combobox/index.tsx (1)
58-58: LGTM!The new
createIconprop cleanly enables customization of the create-option icon while maintaining backward compatibility with thePlusdefault.Also applies to: 98-98, 294-294
packages/ui/src/filter/filter-list.tsx (1)
268-291: LGTM!The empty-state logic correctly handles different scenarios: prompts users to start typing when the filter has no options, defers to
onCreatewhen a search term exists, and shows "No matching options" for standard filtered lists.apps/web/app/(ee)/api/customers/count/route.ts (1)
65-65: Verify the 10,000 link limit is sufficient.The
take: 10000limit prevents unbounded queries when grouping bylinkId. Ensure this limit is sufficient for your largest workspaces and won't cause incomplete data in the UI.packages/prisma/schema/customer.prisma (1)
28-28: Database provider is MySQL, not PostgreSQLโreview comment is incorrect.The schema uses MySQL (
datasource db { provider = "mysql" }), not PostgreSQL. The@@fulltextindex is properly supported via the enabled "fullTextIndex" preview feature in Prisma. MySQL has native full-text search; the review comment's verification script searches for PostgreSQL-specific GIN indexes (USING gin), which are not applicable here. The schema changes are correctly configured.Likely an incorrect or invalid review comment.
Summary by CodeRabbit
New Features
UI / UX
Performance