Updated to use optimized search columns when getting user counts

This commit is contained in:
Ali Waseem committed 2025-11-03 13:47:28 -07:00
1 parent 3ebc95e417
commit 9eb549e34e
8 files changed
+124 -71

No files matched your search

@@ -9,6 +9,7 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { Filter } from './Users.constants'
import { OptimizedSearchColumns } from '@supabase/pg-meta/src/sql/studio/get-users-types'
interface UsersFooterProps {
filter: Filter
@@ -42,6 +43,13 @@ export const UsersFooter = ({
filter: filter === 'all' ? undefined : filter,
providers: selectedProviders,
forceExactCount,
// Force exact count for optimized search since result sets are small
// forceExactCount: forceExactCount || specificFilterColumn !== 'freeform',
// Use optimized search when filtering by specific column
...(specificFilterColumn !== 'freeform'
? { column: specificFilterColumn as OptimizedSearchColumns }
: { column: undefined }),
},
{ keepPreviousData: true }
)
@@ -54,6 +54,9 @@ import {
import { formatUserColumns, formatUsersData } from './Users.utils'
import { UsersFooter } from './UsersFooter'
import { UsersSearch } from './UsersSearch'
import { OptimizedSearchColumns } from '@supabase/pg-meta/src/sql/studio/get-users-types'
type SpecificFilterColumn = OptimizedSearchColumns | 'freeform'
const SORT_BY_VALUE_COUNT_THRESHOLD = 10_000
@@ -91,9 +94,9 @@ export const UsersV2 = () => {
}
}, [showEmailPhoneColumns])
const [specificFilterColumn, setSpecificFilterColumn] = useQueryState(
const [specificFilterColumn, setSpecificFilterColumn] = useQueryState<SpecificFilterColumn>(
'filter',
parseAsStringEnum(['id', 'email', 'phone', 'freeform']).withDefault('id')
parseAsStringEnum<SpecificFilterColumn>(['id', 'email', 'phone', 'freeform']).withDefault('id')
)
const [filterUserType, setFilterUserType] = useQueryState(
'userType',
+3
View File
@@ -1,3 +1,5 @@
import type { OptimizedSearchColumns } from '@supabase/pg-meta/src/sql/studio/get-users-types'
export const authKeys = {
users: (
projectRef: string | undefined,
@@ -39,6 +41,7 @@ export const authKeys = {
filter: string | undefined
providers: string[] | undefined
forceExactCount?: boolean
column?: OptimizedSearchColumns
}
) =>
['projects', projectRef, 'users-count', ...(params ? [params].filter(Boolean) : [])] as const,
+9 -1
View File
@@ -1,3 +1,4 @@
import type { OptimizedSearchColumns } from '@supabase/pg-meta/src/sql/studio/get-users-types'
import { getUsersCountSQL } from '@supabase/pg-meta/src/sql/studio/get-users-count'
import { useQuery } from '@tanstack/react-query'
@@ -13,6 +14,9 @@ type UsersCountVariables = {
filter?: Filter
providers?: string[]
forceExactCount?: boolean
/** If set, uses optimized prefix search for the specified column */
column?: OptimizedSearchColumns
}
export async function getUsersCount(
@@ -23,10 +27,11 @@ export async function getUsersCount(
filter,
providers,
forceExactCount,
column,
}: UsersCountVariables,
signal?: AbortSignal
) {
const sql = getUsersCountSQL({ filter, keywords, providers, forceExactCount })
const sql = getUsersCountSQL({ filter, keywords, providers, forceExactCount, column })
const { result } = await executeSql(
{
@@ -63,6 +68,7 @@ export const useUsersCountQuery = <TData = UsersCountData>(
filter,
providers,
forceExactCount,
column,
}: UsersCountVariables,
{ enabled = true, ...options }: UseCustomQueryOptions<UsersCountData, UsersCountError, TData> = {}
) =>
@@ -72,6 +78,7 @@ export const useUsersCountQuery = <TData = UsersCountData>(
filter,
providers,
forceExactCount,
column,
}),
queryFn: ({ signal }) =>
getUsersCount(
@@ -82,6 +89,7 @@ export const useUsersCountQuery = <TData = UsersCountData>(
filter,
providers,
forceExactCount,
column,
},
signal
),
@@ -0,0 +1,42 @@
export function prefixToUUID(prefix: string, max: boolean) {
const mapped = '00000000-0000-0000-0000-000000000000'
.split('')
.map((c, i) => (c === '-' ? c : prefix[i] ?? c))
if (prefix.length >= mapped.length) {
return mapped.join('')
}
if (prefix.length && prefix.length < 15) {
mapped[14] = '4'
}
if (prefix.length && prefix.length < 20) {
mapped[19] = max ? 'b' : '8'
}
if (max) {
for (let i = prefix.length; i < mapped.length; i += 1) {
if (mapped[i] === '0') {
mapped[i] = 'f'
}
}
}
return mapped.join('')
}
export function stringRange(prefix: string) {
if (!prefix) {
return [prefix, undefined]
}
const lastChar = prefix.charCodeAt(prefix.length - 1)
if (lastChar >= `~`.charCodeAt(0)) {
// not ASCII
return [prefix, prefix]
}
return [prefix, prefix.substring(0, prefix.length - 1) + String.fromCharCode(lastChar + 1)]
}
@@ -1,4 +1,6 @@
import type { OptimizedSearchColumns } from './get-users-types'
import { COUNT_ESTIMATE_SQL, THRESHOLD_COUNT } from './get-count-estimate'
import { stringRange, prefixToUUID } from './get-users-common'
export const USERS_COUNT_ESTIMATE_SQL = `select reltuples as estimate from pg_class where oid = 'auth.users'::regclass`
@@ -7,11 +9,14 @@ export const getUsersCountSQL = ({
keywords,
providers,
forceExactCount = false,
column,
}: {
filter?: 'verified' | 'unverified' | 'anonymous'
keywords?: string
providers?: string[]
forceExactCount?: boolean
/** If set, uses optimized prefix search for the specified column */
column?: OptimizedSearchColumns
}) => {
const hasValidKeywords = keywords && keywords !== ''
@@ -19,33 +24,56 @@ export const getUsersCountSQL = ({
const baseQueryCount = `select count(*) from auth.users`
const baseQuerySelect = `select * from auth.users`
if (hasValidKeywords) {
// [Joshen] Escape single quotes properly
const optimizedSearchMode = column && hasValidKeywords
if (optimizedSearchMode) {
const formattedKeywords = keywords.replaceAll("'", "''")
conditions.push(
`id::text ilike '%${formattedKeywords}%' or email ilike '%${formattedKeywords}%' or phone ilike '%${formattedKeywords}%'`
)
}
if (filter === 'verified') {
conditions.push(`email_confirmed_at IS NOT NULL or phone_confirmed_at IS NOT NULL`)
} else if (filter === 'anonymous') {
conditions.push(`is_anonymous is true`)
} else if (filter === 'unverified') {
conditions.push(`email_confirmed_at IS NULL AND phone_confirmed_at IS NULL`)
}
if (column === 'email') {
const range = stringRange(formattedKeywords)
conditions.push(
`lower(email) >= '${range[0]}'${range[1] ? ` and lower(email) < '${range[1]}'` : ''} and instance_id = '00000000-0000-0000-0000-000000000000'::uuid`
)
} else if (column === 'phone') {
const range = stringRange(formattedKeywords)
conditions.push(`phone >= '${range[0]}'${range[1] ? ` and phone < '${range[1]}'` : ''}`)
} else if (column === 'id') {
const isMatchingUUIDValue = prefixToUUID(formattedKeywords, false) === formattedKeywords
conditions.push(
isMatchingUUIDValue
? `id = '${formattedKeywords}'`
: `id >= '${prefixToUUID(formattedKeywords, false)}' and id < '${prefixToUUID(formattedKeywords, true)}'`
)
}
} else {
// Unified search mode - apply all filters
if (hasValidKeywords) {
// [Joshen] Escape single quotes properly
const formattedKeywords = keywords.replaceAll("'", "''")
conditions.push(
`id::text ilike '%${formattedKeywords}%' or email ilike '%${formattedKeywords}%' or phone ilike '%${formattedKeywords}%'`
)
}
if (providers && providers.length > 0) {
// [Joshen] This is arguarbly not fully optimized, but at the same time not commonly used
// JFYI in case we do eventually run into performance issues here when filtering for SAML provider
if (providers.includes('saml 2.0')) {
conditions.push(
`(select jsonb_agg(case when value ~ '^sso' then 'sso' else value end) from jsonb_array_elements_text((raw_app_meta_data ->> 'providers')::jsonb)) ?| array[${providers.map((p) => (p === 'saml 2.0' ? `'sso'` : `'${p}'`)).join(', ')}]`.trim()
)
} else {
conditions.push(
`(raw_app_meta_data->>'providers')::jsonb ?| array[${providers.map((p) => `'${p}'`).join(', ')}]`
)
if (filter === 'verified') {
conditions.push(`email_confirmed_at IS NOT NULL or phone_confirmed_at IS NOT NULL`)
} else if (filter === 'anonymous') {
conditions.push(`is_anonymous is true`)
} else if (filter === 'unverified') {
conditions.push(`email_confirmed_at IS NULL AND phone_confirmed_at IS NULL`)
}
if (providers && providers.length > 0) {
// [Joshen] This is arguarbly not fully optimized, but at the same time not commonly used
// JFYI in case we do eventually run into performance issues here when filtering for SAML provider
if (providers.includes('saml 2.0')) {
conditions.push(
`(select jsonb_agg(case when value ~ '^sso' then 'sso' else value end) from jsonb_array_elements_text((raw_app_meta_data ->> 'providers')::jsonb)) ?| array[${providers.map((p) => (p === 'saml 2.0' ? `'sso'` : `'${p}'`)).join(', ')}]`.trim()
)
} else {
conditions.push(
`(raw_app_meta_data->>'providers')::jsonb ?| array[${providers.map((p) => `'${p}'`).join(', ')}]`
)
}
}
}
@@ -1,3 +1,6 @@
import { stringRange, prefixToUUID } from './get-users-common'
import { OptimizedSearchColumns } from './get-users-types'
interface getPaginatedUsersSQLProps {
page?: number
verified?: 'verified' | 'unverified' | 'anonymous'
@@ -8,55 +11,12 @@ interface getPaginatedUsersSQLProps {
limit?: number
/** If set, uses fast queries but these don't allow any sorting so the above parameters are completely ignored. */
column?: 'id' | 'email' | 'phone'
column?: OptimizedSearchColumns
startAt?: string
}
const DEFAULT_LIMIT = 50
function prefixToUUID(prefix: string, max: boolean) {
const mapped = '00000000-0000-0000-0000-000000000000'
.split('')
.map((c, i) => (c === '-' ? c : prefix[i] ?? c))
if (prefix.length >= mapped.length) {
return mapped.join('')
}
if (prefix.length && prefix.length < 15) {
mapped[14] = '4'
}
if (prefix.length && prefix.length < 20) {
mapped[19] = max ? 'b' : '8'
}
if (max) {
for (let i = prefix.length; i < mapped.length; i += 1) {
if (mapped[i] === '0') {
mapped[i] = 'f'
}
}
}
return mapped.join('')
}
function stringRange(prefix: string) {
if (!prefix) {
return [prefix, undefined]
}
const lastChar = prefix.charCodeAt(prefix.length - 1)
if (lastChar >= `~`.charCodeAt(0)) {
// not ASCII
return [prefix, prefix]
}
return [prefix, prefix.substring(0, prefix.length - 1) + String.fromCharCode(lastChar + 1)]
}
export const getPaginatedUsersSQL = ({
page = 0,
verified,
@@ -0,0 +1 @@
export type OptimizedSearchColumns = 'id' | 'email' | 'phone'