mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 02:15:05 +03:00
* fix: estimate number of users when count is large In the Auth Users table, we always fetch an exact count of users. This can be a problem for projects with many (>50K) users as the count(*) might cause performance issues on the database. We already have logic on the Table Editor to only run automatic count estimates (fetching the exact count only if usr requests it), this change ports the same logic over to Auth Users. * Nit refactor --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
171 lines
4.7 KiB
TypeScript
171 lines
4.7 KiB
TypeScript
import { useQuery, type UseQueryOptions } from '@tanstack/react-query'
|
|
|
|
import { executeSql, type ExecuteSqlError } from 'data/sql/execute-sql-query'
|
|
import { COUNT_ESTIMATE_SQL, THRESHOLD_COUNT } from 'data/table-rows/table-rows-count-query'
|
|
import { authKeys } from './keys'
|
|
import { type Filter } from './users-infinite-query'
|
|
|
|
type UsersCountVariables = {
|
|
projectRef?: string
|
|
connectionString?: string | null
|
|
keywords?: string
|
|
filter?: Filter
|
|
providers?: string[]
|
|
forceExactCount?: boolean
|
|
}
|
|
|
|
const getUsersCountSql = ({
|
|
filter,
|
|
keywords,
|
|
providers,
|
|
forceExactCount = false,
|
|
}: {
|
|
filter?: Filter
|
|
keywords?: string
|
|
providers?: string[]
|
|
forceExactCount?: boolean
|
|
}) => {
|
|
const hasValidKeywords = keywords && keywords !== ''
|
|
|
|
const conditions: string[] = []
|
|
const baseQueryCount = `select count(*) from auth.users`
|
|
const baseQuerySelect = `select * from auth.users`
|
|
|
|
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 (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(', ')}]`
|
|
)
|
|
}
|
|
}
|
|
|
|
const combinedConditions = conditions.map((x) => `(${x})`).join(' and ')
|
|
const whereClause = conditions.length > 0 ? ` where ${combinedConditions}` : ''
|
|
|
|
if (forceExactCount) {
|
|
return `select (${baseQueryCount}${whereClause}), false as is_estimate;`
|
|
} else {
|
|
const selectBaseSql = `${baseQuerySelect}${whereClause}`
|
|
const countBaseSql = `${baseQueryCount}${whereClause}`
|
|
|
|
const escapedSelectSql = selectBaseSql.replaceAll("'", "''")
|
|
|
|
const sql = `
|
|
${COUNT_ESTIMATE_SQL}
|
|
|
|
with approximation as (
|
|
select reltuples as estimate
|
|
from pg_class
|
|
where oid = 'auth.users'::regclass
|
|
)
|
|
select
|
|
case
|
|
when estimate = -1 then (select pg_temp.count_estimate('${escapedSelectSql}'))::int
|
|
when estimate > ${THRESHOLD_COUNT} then ${conditions.length > 0 ? `(select pg_temp.count_estimate('${escapedSelectSql}'))::int` : 'estimate::int'}
|
|
else (${countBaseSql})
|
|
end as count,
|
|
estimate = -1 or estimate > ${THRESHOLD_COUNT} as is_estimate
|
|
from approximation;
|
|
`.trim()
|
|
|
|
return sql
|
|
}
|
|
}
|
|
|
|
export async function getUsersCount(
|
|
{
|
|
projectRef,
|
|
connectionString,
|
|
keywords,
|
|
filter,
|
|
providers,
|
|
forceExactCount,
|
|
}: UsersCountVariables,
|
|
signal?: AbortSignal
|
|
) {
|
|
const sql = getUsersCountSql({ filter, keywords, providers, forceExactCount })
|
|
|
|
const { result } = await executeSql(
|
|
{
|
|
projectRef,
|
|
connectionString,
|
|
sql,
|
|
queryKey: ['users-count'],
|
|
},
|
|
signal
|
|
)
|
|
|
|
const count = result?.[0]?.count
|
|
const isEstimate = result?.[0]?.is_estimate
|
|
|
|
if (typeof count !== 'number') {
|
|
throw new Error('Error fetching users count')
|
|
}
|
|
|
|
return {
|
|
count,
|
|
is_estimate: isEstimate ?? true,
|
|
}
|
|
}
|
|
|
|
export type UsersCountData = Awaited<ReturnType<typeof getUsersCount>>
|
|
export type UsersCountError = ExecuteSqlError
|
|
|
|
export const useUsersCountQuery = <TData = UsersCountData>(
|
|
{
|
|
projectRef,
|
|
connectionString,
|
|
keywords,
|
|
filter,
|
|
providers,
|
|
forceExactCount,
|
|
}: UsersCountVariables,
|
|
{ enabled = true, ...options }: UseQueryOptions<UsersCountData, UsersCountError, TData> = {}
|
|
) =>
|
|
useQuery<UsersCountData, UsersCountError, TData>(
|
|
authKeys.usersCount(projectRef, {
|
|
keywords,
|
|
filter,
|
|
providers,
|
|
forceExactCount,
|
|
}),
|
|
({ signal }) =>
|
|
getUsersCount(
|
|
{
|
|
projectRef,
|
|
connectionString,
|
|
keywords,
|
|
filter,
|
|
providers,
|
|
forceExactCount,
|
|
},
|
|
signal
|
|
),
|
|
{
|
|
enabled: enabled && typeof projectRef !== 'undefined',
|
|
...options,
|
|
}
|
|
)
|