Files
supabase/apps/studio/data/table-rows/table-rows-count-query.ts
T
Joshen Lim f34fdd6c8f Skip using count estimate function for retrieving row counts if in read only context (#47761)
## 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 -->
2026-07-09 18:19:39 +08:00

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,
})
}