Files
supabase/apps/studio/data/privileges/column-privileges-query.ts
Charis ec64135f9d perf(pg-meta): scope column privileges query to a single table (#48553)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Performance improvement

## What is the current behavior?

The column privileges page in Studio only ever renders one table, but
the underlying query still `aclexplode`s every column in the whole
schema and filters the result client-side.

## What is the new behavior?

Adds a scoped SQL path that prunes `pg_class`/`pg_namespace` to the
requested schema+table before exploding ACLs, gated behind the
`pgMetaScopedIntrospection` flag, with a plan-guard test asserting
`pg_class`/`pg_attribute` stay index-driven. Studio's query hook and
cache keys now thread the selected table through so column-privilege
invalidation and cold-load races are scoped correctly, and the page
fetches per-table instead of per-schema.

## Additional context

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Improvements**
* Column privileges are now scoped to the selected schema and table for
more accurate results.
* Changing schemas automatically updates the table selection and
refreshes the displayed privileges.
* Privilege updates now refresh only the relevant schema, table, and
column data.
* Loading states are handled more accurately when no table is selected.

* **Bug Fixes**
* Improved consistency between scoped and unscoped column privilege
results, including table-, column-, and grant-option privileges.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-31 15:26:37 -04:00

72 lines
2.6 KiB
TypeScript

import pgMeta from '@supabase/pg-meta'
import { useQuery } from '@tanstack/react-query'
import { z } from 'zod'
import { executeSql } from '../sql/execute-sql-mutation'
import { privilegeKeys } from './keys'
import type { components } from '@/data/api'
import { isScopedIntrospection, scopedIntrospectionReady } from '@/data/scoped-introspection'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
export type ColumnPrivilegesVariables = {
projectRef?: string
connectionString?: string | null
/**
* Required to keep this off the whole-catalog path: without it the query
* aclexplodes every relation in the database across every one of its columns.
*/
schema: string
/**
* Restricts to a single relation. The UI only ever renders one table at a
* time, so this keeps the query scoped to what's on screen -- without it the
* whole schema's columns are fetched and all but one table discarded. The
* query stays disabled until a table is selected.
*/
table?: string
}
export type ColumnPrivilege = components['schemas']['PostgresColumnPrivileges']
const pgMetaColumnPrivilegesList = pgMeta.columnPrivileges.list()
type ColumnPrivilegesData = z.infer<typeof pgMetaColumnPrivilegesList.zod>
export async function getColumnPrivileges(
{ projectRef, connectionString, schema, table }: ColumnPrivilegesVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
// Cold-load race guard -- see the module comment on scoped-introspection.ts.
await scopedIntrospectionReady()
const sql = pgMeta.columnPrivileges.list({
includedSchemas: [schema],
relationName: table,
scoped: isScopedIntrospection(),
}).sql
const queryKey = ['column-privileges', schema, table]
const { result } = await executeSql({ projectRef, connectionString, sql, queryKey }, signal)
return result as ColumnPrivilegesData
}
export type ColumnPrivilegesError = ResponseError
export const useColumnPrivilegesQuery = <TData = ColumnPrivilegesData>(
vars: ColumnPrivilegesVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions<ColumnPrivilegesData, ColumnPrivilegesError, TData> = {}
) => {
const { projectRef, schema, table } = vars
return useQuery<ColumnPrivilegesData, ColumnPrivilegesError, TData>({
queryKey: privilegeKeys.columnPrivilegesList(projectRef, schema, table),
queryFn: ({ signal }) => getColumnPrivileges(vars, signal),
enabled:
enabled &&
typeof projectRef !== 'undefined' &&
typeof schema !== 'undefined' &&
typeof table !== 'undefined',
...options,
})
}