Files
supabase/apps/studio/data/table-rows/table-rows-count-query.ts
Joshen Lim eb20a4674d getTableDefinitionSql to escape SQL identifiers (#51258)
## Context

Similar to domain to https://github.com/supabase/supabase/pull/51256 -
`getTableDefinitionSql` doesn't escape SQL identifiers, which generates
invalid SQL on the dashboard's table editor for the "Copy table schema"
CTA, or the table definition tab.

Also fixes the "Copy table schema" CTA which was missing the `scoped`
parameter when calling `getTableDefinition`

Changes here addresses this issue, can test with a table named like
`test"table`
2026-10-06 21:34:36 +08:00

157 lines
4.7 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, useFlag } 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 {
PG_META_SCOPED_INTROSPECTION_FLAG,
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
scoped?: boolean
}
export type TableRowsCountData = TableRowsCount
export type TableRowsCountError = ResponseError
export async function getTableRowsCount(
{
queryClient,
projectRef,
connectionString,
tableId,
filters,
roleImpersonationState,
enforceExactCount,
isReadOnlyContext = false,
scoped,
}: TableRowsCountVariables & { isReadOnlyContext?: boolean },
signal?: AbortSignal
) {
const entity = await prefetchTableEditor(queryClient, {
projectRef,
connectionString,
id: tableId,
scoped,
})
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,
scoped,
}),
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, isLoading: isPermissionsLoading } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'tables'
)
const scoped = useFlag(PG_META_SCOPED_INTROSPECTION_FLAG)
return useQuery<TableRowsCountData, TableRowsCountError, TData>({
queryKey: tableRowKeys.tableRowsCount(projectRef, {
table: { id: tableId },
readReplicaIdentifier,
...args,
scoped,
}),
queryFn: ({ signal }) =>
getTableRowsCount(
{
queryClient,
projectRef,
connectionString,
tableId,
isReadOnlyContext: type === 'replica' || !canSQLAdminWrite,
...args,
scoped,
},
signal
),
enabled:
enabled &&
typeof projectRef !== 'undefined' &&
typeof tableId !== 'undefined' &&
(!IS_PLATFORM || typeof connectionString !== 'undefined') &&
// isReadOnlyContext resolves to `type === 'replica' || !canSQLAdminWrite`: for
// read replicas it's already known synchronously, but otherwise it depends on
// canSQLAdminWrite, which starts out `false` while permissions are loading.
// Firing while that's still in flight would cache a transient
// isReadOnlyContext:true (and, on a never-analyzed table, a scoped
// count:-1/is_estimate:true) for what may actually be a writable user. Wait
// for the permission check to settle before firing in that case.
(type === 'replica' || !isPermissionsLoading),
...options,
})
}