From 205dac33daa0a735fa101cd756765bf1d375417a Mon Sep 17 00:00:00 2001 From: Aaron Byrne <65355719+aaronByrne1@users.noreply.github.com> Date: Mon, 6 Apr 2026 09:57:08 -0400 Subject: [PATCH] feat: Adding a more lightweight query to pg-meta that just fetches the table names (#44469) Bug fix & feature ## What is the current behavior? Customers with large schemas have trouble running: pg-meta/{ref}/tables?include_columns=false ## What is the new behavior? In the webhooks view some users with large schemas could not click a table name as the underlying query times out. This just adds a light weight query and fetches the table name ## Additional context Add any other context or screenshots. ## Summary by CodeRabbit * **Refactor** * Optimized table name fetching in the database interface by introducing an enhanced query mechanism that streams table metadata more efficiently from the database. --- .../Database/Hooks/FormContents.tsx | 11 +--- apps/studio/data/tables/keys.ts | 1 + apps/studio/data/tables/table-names-query.ts | 59 +++++++++++++++++++ 3 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 apps/studio/data/tables/table-names-query.ts diff --git a/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx b/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx index f7f95fb71a3..551d90076ad 100644 --- a/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx @@ -2,7 +2,7 @@ import type { PostgresTrigger } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import Image from 'next/legacy/image' -import { useEffect, useMemo } from 'react' +import { useEffect } from 'react' import { UseFormReturn } from 'react-hook-form' import { Checkbox_Shadcn_, @@ -35,7 +35,7 @@ import { } from '@/components/ui/Forms/FormSection' import { useAPIKeysQuery } from '@/data/api-keys/api-keys-query' import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query' -import { useTablesQuery } from '@/data/tables/tables-query' +import { useTableNamesQuery } from '@/data/tables/table-names-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { uuidv4 } from '@/lib/helpers' @@ -66,16 +66,11 @@ export const FormContents = ({ form, selectedHook }: FormContentsProps) => { const httpUrl = useWatch_Shadcn_({ control: form.control, name: 'http_url' }) const httpHeaders = useWatch_Shadcn_({ control: form.control, name: 'httpHeaders' }) - const { data } = useTablesQuery({ + const { data: tables = [] } = useTableNamesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) - const tables = useMemo( - () => [...(data ?? [])].sort((a, b) => (a.schema > b.schema ? 0 : -1)), - [data] - ) - // Handle auth header auto-add for edge functions useEffect(() => { if (!isSuccessEdgeFunctions) return diff --git a/apps/studio/data/tables/keys.ts b/apps/studio/data/tables/keys.ts index 02d4221fe80..da37b9757b1 100644 --- a/apps/studio/data/tables/keys.ts +++ b/apps/studio/data/tables/keys.ts @@ -1,4 +1,5 @@ export const tableKeys = { + names: (projectRef: string | undefined) => ['projects', projectRef, 'table-names'] as const, list: (projectRef: string | undefined, schema?: string, includeColumns?: boolean) => ['projects', projectRef, 'tables', schema, includeColumns].filter(Boolean), retrieve: (projectRef: string | undefined, name: string, schema: string) => diff --git a/apps/studio/data/tables/table-names-query.ts b/apps/studio/data/tables/table-names-query.ts new file mode 100644 index 00000000000..6142ff3bcb7 --- /dev/null +++ b/apps/studio/data/tables/table-names-query.ts @@ -0,0 +1,59 @@ +import { useQuery } from '@tanstack/react-query' +import type { UseCustomQueryOptions } from 'types' + +import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query' +import { tableKeys } from './keys' + +export type TableName = { + id: number + schema: string + name: string +} + +export type TableNamesVariables = { + projectRef?: string + connectionString?: string | null +} + +const TABLE_NAMES_SQL = /* sql */ ` +select + c.oid::int8 as id, + nc.nspname as schema, + c.relname as name +from pg_namespace nc +join pg_class c on nc.oid = c.relnamespace +where c.relkind in ('r', 'p') + and not pg_is_other_temp_schema(nc.oid) + and nc.nspname not in ('information_schema', 'pg_catalog', 'pg_toast') + and ( + pg_has_role(c.relowner, 'USAGE') + or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') + or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') + ) +order by nc.nspname, c.relname +`.trim() + +export async function getTableNames( + { projectRef, connectionString }: TableNamesVariables, + signal?: AbortSignal +) { + const { result } = await executeSql( + { projectRef, connectionString, sql: TABLE_NAMES_SQL, queryKey: ['table-names'] }, + signal + ) + return result +} + +export type TableNamesData = Awaited> +export type TableNamesError = ExecuteSqlError + +export const useTableNamesQuery = ( + { projectRef, connectionString }: TableNamesVariables, + { enabled = true, ...options }: UseCustomQueryOptions = {} +) => + useQuery({ + queryKey: tableKeys.names(projectRef), + queryFn: ({ signal }) => getTableNames({ projectRef, connectionString }, signal), + enabled: enabled && typeof projectRef !== 'undefined', + ...options, + })