mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 03:15:06 +03:00
## Context Currently when retrieving row counts of a table in the Table Editor, we're using a `COUNT_ESTIMATE` pg function ([ref](https://github.com/supabase/supabase/blob/master/packages/pg-meta/src/sql/studio/database/get-count-estimate.ts#L5)) to retrieve an estimate (instead of checking `pg_class` -> `reltuples`) as that would theoretically provide a more accurate representation. However, in a read only context, that function can't be used - users will run into `cannot execute CREATE FUNCTION in a read-only transaction`, so we need to fallback to just checking `pg_class` in this scenario. The logic's already set up as we were previously looking into allowing users to use a read replica to power the dashboard, but we also need to consider members with read-only roles within the organization, so this PR updates the logic a little to factor that in. ## To test - [ ] With a read-only role, open the table editor and verify that we're not using the count estimate function to retrieve the table row counts <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Updated the invite member dialog to open in a larger size for better usability. * **Bug Fixes** * Improved table row count behavior so it now respects read-only access and permission limits more reliably. * Count estimates should now be shown more consistently across different database contexts. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
139 lines
3.9 KiB
TypeScript
139 lines
3.9 KiB
TypeScript
import { getTableRowsCountSql } from '@supabase/pg-meta'
|
|
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { IS_PLATFORM } from 'common'
|
|
|
|
import { tableRowKeys } from './keys'
|
|
import { formatFilterValue } from './utils'
|
|
import { parseSupaTable } from '@/components/grid/SupabaseGrid.utils'
|
|
import type { Filter, SupaTable } from '@/components/grid/types'
|
|
import { useConnectionStringForReadOps } from '@/data/read-replicas/replicas-query'
|
|
import { executeSql } from '@/data/sql/execute-sql-mutation'
|
|
import { prefetchTableEditor } from '@/data/table-editor/table-editor-query'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { RoleImpersonationState, wrapWithRoleImpersonation } from '@/lib/role-impersonation'
|
|
import { isRoleImpersonationEnabled } from '@/state/role-impersonation-state'
|
|
import { ResponseError, UseCustomQueryOptions } from '@/types'
|
|
|
|
export type GetTableRowsCountArgs = {
|
|
table?: SupaTable
|
|
filters?: Filter[]
|
|
enforceExactCount?: boolean
|
|
}
|
|
|
|
export type TableRowsCount = {
|
|
count?: number
|
|
is_estimate?: boolean
|
|
}
|
|
|
|
export type TableRowsCountVariables = Omit<GetTableRowsCountArgs, 'table'> & {
|
|
queryClient: QueryClient
|
|
tableId?: number
|
|
roleImpersonationState?: RoleImpersonationState
|
|
projectRef?: string
|
|
connectionString?: string | null
|
|
}
|
|
|
|
export type TableRowsCountData = TableRowsCount
|
|
export type TableRowsCountError = ResponseError
|
|
|
|
export async function getTableRowsCount(
|
|
{
|
|
queryClient,
|
|
projectRef,
|
|
connectionString,
|
|
tableId,
|
|
filters,
|
|
roleImpersonationState,
|
|
enforceExactCount,
|
|
isReadOnlyContext = false,
|
|
}: TableRowsCountVariables & { isReadOnlyContext?: boolean },
|
|
signal?: AbortSignal
|
|
) {
|
|
const entity = await prefetchTableEditor(queryClient, {
|
|
projectRef,
|
|
connectionString,
|
|
id: tableId,
|
|
})
|
|
if (!entity) {
|
|
throw new Error('Table not found')
|
|
}
|
|
|
|
const table = parseSupaTable(entity)
|
|
|
|
const formattedFilters = filters?.map((x) => ({ ...x, value: formatFilterValue(table, x) }))
|
|
const sql = wrapWithRoleImpersonation(
|
|
getTableRowsCountSql({
|
|
table,
|
|
filters: formattedFilters,
|
|
enforceExactCount,
|
|
isReadOnlyContext,
|
|
}),
|
|
roleImpersonationState
|
|
)
|
|
const { result } = await executeSql(
|
|
{
|
|
projectRef,
|
|
connectionString,
|
|
sql,
|
|
queryKey: ['table-rows-count', table.id],
|
|
isRoleImpersonationEnabled: isRoleImpersonationEnabled(roleImpersonationState?.role),
|
|
},
|
|
signal
|
|
)
|
|
|
|
return {
|
|
count: result?.[0]?.count,
|
|
is_estimate: result?.[0]?.is_estimate ?? false,
|
|
} as TableRowsCount
|
|
}
|
|
|
|
export const useTableRowsCountQuery = <TData = TableRowsCountData>(
|
|
{
|
|
projectRef,
|
|
tableId,
|
|
...args
|
|
}: Omit<TableRowsCountVariables, 'queryClient' | 'connectionString'>,
|
|
{
|
|
enabled = true,
|
|
...options
|
|
}: UseCustomQueryOptions<TableRowsCountData, TableRowsCountError, TData> = {}
|
|
) => {
|
|
const queryClient = useQueryClient()
|
|
const {
|
|
connectionString,
|
|
identifier: readReplicaIdentifier,
|
|
type,
|
|
} = useConnectionStringForReadOps()
|
|
const { can: canSQLAdminWrite } = useAsyncCheckPermissions(
|
|
PermissionAction.TENANT_SQL_ADMIN_WRITE,
|
|
'tables'
|
|
)
|
|
|
|
return useQuery<TableRowsCountData, TableRowsCountError, TData>({
|
|
queryKey: tableRowKeys.tableRowsCount(projectRef, {
|
|
table: { id: tableId },
|
|
readReplicaIdentifier,
|
|
...args,
|
|
}),
|
|
queryFn: ({ signal }) =>
|
|
getTableRowsCount(
|
|
{
|
|
queryClient,
|
|
projectRef,
|
|
connectionString,
|
|
tableId,
|
|
isReadOnlyContext: type === 'replica' || !canSQLAdminWrite,
|
|
...args,
|
|
},
|
|
signal
|
|
),
|
|
enabled:
|
|
enabled &&
|
|
typeof projectRef !== 'undefined' &&
|
|
typeof tableId !== 'undefined' &&
|
|
(!IS_PLATFORM || typeof connectionString !== 'undefined'),
|
|
...options,
|
|
})
|
|
}
|