From f34fdd6c8fba66c21a7842fc863dd4e6d1af4fbc Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 9 Jul 2026 18:19:39 +0800 Subject: [PATCH] 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 ## 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. --- .../TeamSettings/InviteMemberButton.tsx | 2 +- .../data/table-rows/table-rows-count-query.ts | 14 ++++++++++---- packages/pg-meta/src/sql/studio/database/rows.ts | 7 ++++--- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx index 140fb26a64f..c5d1fab5de4 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx @@ -280,7 +280,7 @@ export const InviteMemberButton = () => { - + Invite team members diff --git a/apps/studio/data/table-rows/table-rows-count-query.ts b/apps/studio/data/table-rows/table-rows-count-query.ts index 3933afd0c73..111be34e74c 100644 --- a/apps/studio/data/table-rows/table-rows-count-query.ts +++ b/apps/studio/data/table-rows/table-rows-count-query.ts @@ -1,4 +1,5 @@ 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' @@ -9,6 +10,7 @@ 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' @@ -44,8 +46,8 @@ export async function getTableRowsCount( filters, roleImpersonationState, enforceExactCount, - isUsingReadReplica = false, - }: TableRowsCountVariables & { isUsingReadReplica?: boolean }, + isReadOnlyContext = false, + }: TableRowsCountVariables & { isReadOnlyContext?: boolean }, signal?: AbortSignal ) { const entity = await prefetchTableEditor(queryClient, { @@ -65,7 +67,7 @@ export async function getTableRowsCount( table, filters: formattedFilters, enforceExactCount, - isUsingReadReplica, + isReadOnlyContext, }), roleImpersonationState ) @@ -103,6 +105,10 @@ export const useTableRowsCountQuery = ( identifier: readReplicaIdentifier, type, } = useConnectionStringForReadOps() + const { can: canSQLAdminWrite } = useAsyncCheckPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'tables' + ) return useQuery({ queryKey: tableRowKeys.tableRowsCount(projectRef, { @@ -117,7 +123,7 @@ export const useTableRowsCountQuery = ( projectRef, connectionString, tableId, - isUsingReadReplica: type === 'replica', + isReadOnlyContext: type === 'replica' || !canSQLAdminWrite, ...args, }, signal diff --git a/packages/pg-meta/src/sql/studio/database/rows.ts b/packages/pg-meta/src/sql/studio/database/rows.ts index c4657bbe2a0..ad6e7517453 100644 --- a/packages/pg-meta/src/sql/studio/database/rows.ts +++ b/packages/pg-meta/src/sql/studio/database/rows.ts @@ -12,12 +12,13 @@ export const getTableRowsCountSql = ({ table, filters = [], enforceExactCount = false, - isUsingReadReplica = false, + isReadOnlyContext = false, }: { table: any filters?: Filter[] enforceExactCount?: boolean - isUsingReadReplica?: boolean + /** Skips using the count estimate function if true and fallsback to checking reltuples from pg_class */ + isReadOnlyContext?: boolean }): SafeSqlFragment => { if (!table) return safeSql`` @@ -59,7 +60,7 @@ export const getTableRowsCountSql = ({ ? (countBaseSql.slice(0, -1) as SafeSqlFragment) : countBaseSql - if (isUsingReadReplica) { + if (isReadOnlyContext) { const sql = safeSql` with approximation as ( select reltuples as estimate