From aba4e147eb1eaae25dd86c3b0b423651f02ec4ff Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 16 Jun 2026 15:45:39 +0800 Subject: [PATCH] Joshen/fe 3613 database tables query should have schema filter wherever appropriate (#46935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context There's certain areas in the dashboard where we're calling `useTablesQuery` without a schema filter, in which case the dashboard then fires a query against the project's database to fetch _all_ tables across _all_ schemas - this could easily be a heavy query if there's a large number of relations in the project's database. Am hence opting to either add a schema filter if appropriate, or otherwise opt to use the infinite loading behaviour ## Changes involved - Add schema filter to `useTablesQuery` in database triggers and publications - Use infinite loading for tables in Cmd K for "Run query on table" and "Search database tables" ## To test - [x] Verify that database triggers + publications still function as expected - [x] Verify that CMD K "Run query on table" and "Search database tables" still function as expected (including search) ## Summary by CodeRabbit * **New Features** * Implemented debounced infinite-scrolling table search in the command menu and SQL editor command flow. * Added a schema selector dropdown to publications management for easier navigation. * **Improvements** * Removed the “Schema” column from the publications tables UI. * Updated search guidance and table-picker status (counts/loading) during infinite browsing. * Trigger table listings now follow the selected schema context. * Refined command menu list height and improved the database-tables placeholder text. --- .../App/CommandMenu/ContextSearchCommands.tsx | 22 ++- .../ContextSearchResults.shared.tsx | 7 +- .../App/CommandMenu/ContextSearchResults.tsx | 9 +- .../App/CommandMenu/TableSearchResults.tsx | 182 ++++++++---------- .../Publications/PublicationsTableItem.tsx | 1 - .../Publications/PublicationsTables.tsx | 32 ++- .../Database/Triggers/TriggerSheet.tsx | 3 + .../Triggers/TriggersList/TriggersList.tsx | 1 + .../SidePanelEditor/SidePanelEditor.tsx | 12 +- .../SidePanelEditor/SidePanelEditor.utils.tsx | 4 +- .../TableEditor/TableEditor.tsx | 4 +- .../SQLEditorLayout/SqlEditor.Commands.tsx | 90 +++++++-- .../ui/AIAssistantPanel/AIAssistant.utils.ts | 5 +- apps/studio/data/tables/keys.ts | 7 +- .../data/tables/table-create-mutation.ts | 4 +- apps/studio/data/tables/tables-query.ts | 70 +++---- .../sql/studio/database/tables-paginated.ts | 7 +- .../ui/src/components/shadcn/ui/command.tsx | 2 +- 18 files changed, 267 insertions(+), 195 deletions(-) diff --git a/apps/studio/components/interfaces/App/CommandMenu/ContextSearchCommands.tsx b/apps/studio/components/interfaces/App/CommandMenu/ContextSearchCommands.tsx index 7e8f5718a51..83060ab341d 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/ContextSearchCommands.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/ContextSearchCommands.tsx @@ -1,9 +1,10 @@ 'use client' +import { useDebounce } from '@uidotdev/usehooks' import { IS_PLATFORM } from 'common' import { Auth, EdgeFunctions, Storage } from 'icons' import { Database } from 'lucide-react' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import type { ICommand } from 'ui-patterns/CommandMenu' import { CommandHeader, @@ -36,7 +37,7 @@ const SEARCH_CONTEXT_OPTIONS: SearchContextOption[] = [ value: 'database-tables', label: 'Database Tables', pageName: 'Search Database Tables', - placeholder: 'Search database tables...', + placeholder: 'Search database schema or tables...', icon: Database, }, { @@ -71,12 +72,23 @@ function ContextSearchPage({ }) { const query = useQuery() + const [filterString, setFilterString] = useState('') + const debouncedFilterString = useDebounce(filterString, 300) + return ( - + - + - + ) } diff --git a/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.shared.tsx b/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.shared.tsx index d798f407c50..6e410076842 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.shared.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.shared.tsx @@ -1,5 +1,3 @@ -'use client' - import { cn, CommandList } from 'ui' import { ShimmeringLoader } from 'ui-patterns' import { TextHighlighter } from 'ui-patterns/CommandMenu' @@ -53,6 +51,7 @@ interface ResultsListProps { onResultClick?: (result: SearchResult) => void getRoute?: (result: SearchResult) => `/${string}` | `http${string}` className?: string + infiniteLoadingObserverRef?: (node: Element | null) => void } export function ResultsList({ @@ -62,6 +61,7 @@ export function ResultsList({ onResultClick, getRoute, className, + infiniteLoadingObserverRef, }: ResultsListProps) { const commands = results.map((result): IRouteCommand | IActionCommand => { const ResultIcon = getIcon ? getIcon(result) : Icon @@ -92,7 +92,7 @@ export function ResultsList({ className )} > - + {commands.map((command) => (
@@ -106,6 +106,7 @@ export function ResultsList({ ))} + {infiniteLoadingObserverRef &&
} ) } diff --git a/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.tsx b/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.tsx index 80701e4458b..205f34fa940 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.tsx @@ -45,6 +45,7 @@ const StorageSearchResults = dynamic( interface ContextSearchResultsProps { context: SearchContextValue query: string + debouncedFilterString: string } const CONTEXT_CONFIG: Record< @@ -77,13 +78,17 @@ const CONTEXT_CONFIG: Record< }, } -export function ContextSearchResults({ context, query }: ContextSearchResultsProps) { +export function ContextSearchResults({ + context, + query, + debouncedFilterString, +}: ContextSearchResultsProps) { const config = CONTEXT_CONFIG[context] if (context === 'database-tables') { return (
- +
) } diff --git a/apps/studio/components/interfaces/App/CommandMenu/TableSearchResults.tsx b/apps/studio/components/interfaces/App/CommandMenu/TableSearchResults.tsx index 66266fcd47b..6861f0ca1dd 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/TableSearchResults.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/TableSearchResults.tsx @@ -1,8 +1,7 @@ -'use client' - +import { useIntersectionObserver } from '@uidotdev/usehooks' import { useParams } from 'common' import { Database, Loader2 } from 'lucide-react' -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import { EmptyState, @@ -10,55 +9,44 @@ import { SkeletonResults, type SearchResult, } from './ContextSearchResults.shared' -import { useTablesQuery } from '@/data/tables/tables-query' +import { useInfiniteTablesQuery } from '@/data/tables/tables-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' interface TableSearchResultsProps { - query: string + debouncedFilterString: string } -export function TableSearchResults({ query }: TableSearchResultsProps) { +export function TableSearchResults({ debouncedFilterString }: TableSearchResultsProps) { const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() - const trimmedQuery = query.trim() + const [sentinelRef, sentinelEntry] = useIntersectionObserver({ + threshold: 0, + rootMargin: '200px 0px 200px 0px', + }) const { - data: tables, - isLoading: isLoadingTables, + data: tablesData, + isSuccess, isError: isErrorTables, - } = useTablesQuery( - { - projectRef: project?.ref, - connectionString: project?.connectionString, - includeColumns: false, - sortByProperty: 'name', - }, - { - enabled: !!project?.ref, - } - ) + isPending: isLoadingTables, + hasNextPage: hasNextTablesPage, + isFetchingNextPage: isFetchingNextTablesPage, + fetchNextPage: fetchNextTablesPage, + } = useInfiniteTablesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + includeColumns: false, + pageSize: 50, + nameFilter: debouncedFilterString, + }) + + const tables = useMemo(() => tablesData?.pages.flat() ?? [], [tablesData]) const tableResults: SearchResult[] = useMemo(() => { if (!tables) return [] - const filtered = trimmedQuery - ? tables.filter((table) => { - const searchLower = trimmedQuery.toLowerCase() - const tableName = table.name?.toLowerCase() || '' - const schemaName = table.schema?.toLowerCase() || '' - const fullName = `${schemaName}.${tableName}` - - return ( - tableName.includes(searchLower) || - schemaName.includes(searchLower) || - fullName.includes(searchLower) - ) - }) - : tables - - // Limit results for performance - return filtered.slice(0, 20).map((table) => { + return tables.map((table) => { const displayName = table.schema && table.schema !== 'public' ? `${table.schema}.${table.name}` @@ -76,79 +64,71 @@ export function TableSearchResults({ query }: TableSearchResultsProps) { description, } }) - }, [tables, trimmedQuery]) + }, [tables]) const totalTables = tables?.length ?? 0 - const renderFooter = () => ( -
-
- {isLoadingTables ? ( - - Loading... - - ) : ( - - Total: {totalTables.toLocaleString()} table{totalTables !== 1 ? 's' : ''} - - )} -
-
- ) - - if (isLoadingTables) { - return ( -
-
- -
- {renderFooter()} -
- ) - } - - if (isErrorTables) { - return ( -
-
-
- -

Failed to load tables

-
-
- {renderFooter()} -
- ) - } - - if (tableResults.length === 0) { - return ( -
-
- -
- {renderFooter()} -
- ) - } + useEffect(() => { + if ( + sentinelEntry?.isIntersecting && + hasNextTablesPage && + !isFetchingNextTablesPage && + isSuccess + ) { + fetchNextTablesPage() + } + }, [ + isSuccess, + sentinelEntry?.isIntersecting, + hasNextTablesPage, + isFetchingNextTablesPage, + fetchNextTablesPage, + ]) return (
- { - const table = tables?.find((t) => String(t.id) === result.id) - if (!table || !projectRef) return `/project/${projectRef}/editor` as `/${string}` + {isLoadingTables ? ( + + ) : isErrorTables ? ( +
+ +

Failed to load tables

+
+ ) : tableResults.length === 0 ? ( + + ) : ( + <> + { + const table = tables?.find((t) => String(t.id) === result.id) + if (!table || !projectRef) return `/project/${projectRef}/editor` as `/${string}` - const schemaParam = table.schema ? `?schema=${table.schema}` : '' - return `/project/${projectRef}/editor/${table.id}${schemaParam}` as `/${string}` - }} - className="pb-9" - /> + const schemaParam = table.schema ? `?schema=${table.schema}` : '' + return `/project/${projectRef}/editor/${table.id}${schemaParam}` as `/${string}` + }} + infiniteLoadingObserverRef={sentinelRef} + className="pb-9" + /> +
+
+ {isLoadingTables ? ( + + Loading... + + ) : ( + + Total: {totalTables.toLocaleString()} table{totalTables !== 1 ? 's' : ''} + {hasNextTablesPage ? ' loaded' : ''} + + )} +
+
+ + )}
- {renderFooter()}
) } diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx index df2e4f2685f..5ef3ea67a85 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx @@ -75,7 +75,6 @@ export const PublicationsTableItem = ({ return ( {table.name} - {table.schema} {table.comment} diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx index 00390fd5d37..60b969f3564 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx @@ -10,9 +10,12 @@ import { PublicationTablesSkeleton } from './PublicationSkeleton' import { PublicationsTableItem } from './PublicationsTableItem' import { AlertError } from '@/components/ui/AlertError' import { NoSearchResults } from '@/components/ui/NoSearchResults' +import SchemaSelector from '@/components/ui/SchemaSelector' +import { Shortcut } from '@/components/ui/Shortcut' import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query' import { useTablesQuery } from '@/data/tables/tables-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' +import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { onSearchInputEscape } from '@/lib/keyboard' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' @@ -21,7 +24,10 @@ import { useShortcut } from '@/state/shortcuts/useShortcut' export const PublicationsTables = () => { const { id } = useParams() const { data: project } = useSelectedProjectQuery() + const { selectedSchema, setSelectedSchema } = useQuerySchemaState() + const [filterString, setFilterString] = useState('') + const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false) const searchInputRef = useRef(null) const { can: canUpdatePublications, isLoading: isLoadingPermissions } = useAsyncCheckPermissions( @@ -53,6 +59,7 @@ export const PublicationsTables = () => { } = useTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, + schema: selectedSchema, }) const tables = useMemo(() => { @@ -63,7 +70,23 @@ export const PublicationsTables = () => { return ( <> -
+
+ setSchemaSelectorOpen(true)} + side="bottom" + tooltipOpen={schemaSelectorOpen ? false : undefined} + > + + { Name - Schema Description {/* We've disabled All tables toggle for publications. @@ -106,7 +128,7 @@ export const PublicationsTables = () => { {isError && ( - + @@ -114,7 +136,7 @@ export const PublicationsTables = () => { {!isLoading && !isLoadingPermissions && tables.length === 0 && ( - + { )) ) : ( - +

The selected publication with ID {id} cannot be found

Head back to the list of publications to select one from there diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx index 63d9499303f..35a1c109877 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx @@ -39,6 +39,7 @@ import FormBoxEmpty from '@/components/ui/FormBoxEmpty' import { useDatabaseTriggerCreateMutation } from '@/data/database-triggers/database-trigger-create-mutation' import { useDatabaseTriggerUpdateMutation } from '@/data/database-triggers/database-trigger-update-mutation' import { useTablesQuery } from '@/data/tables/tables-query' +import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose' import { useProtectedSchemas } from '@/hooks/useProtectedSchemas' @@ -89,6 +90,7 @@ export const TriggerSheet = ({ onClose, }: TriggerSheetProps) => { const { data: project } = useSelectedProjectQuery() + const { selectedSchema } = useQuerySchemaState() const [showFunctionSelector, setShowFunctionSelector] = useState(false) @@ -118,6 +120,7 @@ export const TriggerSheet = ({ const { data = [], isSuccess: isSuccessTables } = useTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, + schema: selectedSchema, }) const { data: protectedSchemas, isSuccess: isSuccessProtectedSchemas } = useProtectedSchemas() const isSuccess = isSuccessTables && isSuccessProtectedSchemas diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx index 5386a44f8e4..c47e1b89520 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx @@ -76,6 +76,7 @@ export const TriggersList = () => { const { data = [] } = useTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, + schema: selectedSchema, }) const hasTables = data.filter((a) => !protectedSchemas.find((s) => s.name === a.schema)).length > 0 diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index f8f08a40881..42d782b88ec 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -437,7 +437,7 @@ export const SidePanelEditor = ({ }), queryClient.invalidateQueries({ queryKey: entityTypeKeys.list(project?.ref) }), queryClient.invalidateQueries({ - queryKey: tableKeys.list(project?.ref, selectedTable?.schema, includeColumns), + queryKey: tableKeys.list(project?.ref, selectedTable?.schema, { includeColumns }), }), queryClient.invalidateQueries({ queryKey: tableKeys.infiniteListPrefix(project?.ref, selectedTable?.schema), @@ -508,7 +508,7 @@ export const SidePanelEditor = ({ // Switch it to individual tables via an array of strings // Refer to PublicationStore for more information about this const publicTables = await queryClient.fetchQuery({ - queryKey: tableKeys.list(project.ref, 'public', includeColumns), + queryKey: tableKeys.list(project.ref, 'public', { includeColumns }), queryFn: ({ signal }) => getTables( { @@ -646,10 +646,10 @@ export const SidePanelEditor = ({ .map(([, data]) => data?.pages?.[0]?.data?.count) .find((count) => typeof count === 'number') ?? queryClient.getQueryData( - tableKeys.list(project?.ref, payload.schema, true) + tableKeys.list(project?.ref, payload.schema, { includeColumns: true }) )?.length ?? queryClient.getQueryData( - tableKeys.list(project?.ref, payload.schema, false) + tableKeys.list(project?.ref, payload.schema, { includeColumns: false }) )?.length createTableSpan.setAttributes({ @@ -713,7 +713,7 @@ export const SidePanelEditor = ({ async () => { await Promise.all([ queryClient.invalidateQueries({ - queryKey: tableKeys.list(project?.ref, table.schema, includeColumns), + queryKey: tableKeys.list(project?.ref, table.schema, { includeColumns }), }), queryClient.invalidateQueries({ queryKey: tableKeys.infiniteListPrefix(project?.ref, table.schema), @@ -783,7 +783,7 @@ export const SidePanelEditor = ({ await Promise.all([ queryClient.invalidateQueries({ - queryKey: tableKeys.list(project?.ref, table.schema, includeColumns), + queryKey: tableKeys.list(project?.ref, table.schema, { includeColumns }), }), queryClient.invalidateQueries({ queryKey: tableKeys.infiniteListPrefix(project?.ref, table.schema), diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx index 7a2b6277ed6..ac043ff81a7 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx @@ -908,7 +908,9 @@ export const updateTable = async ({ }), queryClient.invalidateQueries({ queryKey: databaseKeys.tableDefinition(projectRef, table.id) }), queryClient.invalidateQueries({ queryKey: entityTypeKeys.list(projectRef) }), - queryClient.invalidateQueries({ queryKey: tableKeys.list(projectRef, table.schema, true) }), + queryClient.invalidateQueries({ + queryKey: tableKeys.list(projectRef, table.schema, { includeColumns: true }), + }), queryClient.invalidateQueries({ queryKey: lintKeys.lint(projectRef) }), ]) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx index 2a4c1937870..a7fb5b04002 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx @@ -478,7 +478,7 @@ export const TableEditor = ({ > @@ -496,7 +496,7 @@ export const TableEditor = ({ > diff --git a/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx b/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx index df8be38a8f1..0c95f7a8691 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx @@ -1,10 +1,11 @@ import type { PGColumn } from '@supabase/pg-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useDebounce, useIntersectionObserver } from '@uidotdev/usehooks' import { useParams } from 'common' import { AlertTriangle, Code, Loader2, Table2 } from 'lucide-react' import { useRouter } from 'next/navigation' -import { useEffect, useMemo, useRef } from 'react' -import { cn, CommandEmpty, CommandGroup, CommandItem, CommandList } from 'ui' +import { useEffect, useRef, useState } from 'react' +import { cn, CommandGroup, CommandItem, CommandList } from 'ui' import { CodeBlock } from 'ui-patterns/CodeBlock' import type { CommandOptions } from 'ui-patterns/CommandMenu' import { @@ -26,10 +27,13 @@ import { import { COMMAND_MENU_SECTIONS } from '@/components/interfaces/App/CommandMenu/CommandMenu.utils' import { orderCommandSectionsByPriority } from '@/components/interfaces/App/CommandMenu/ordering' import { useSqlSnippetsQuery, type SqlSnippet } from '@/data/content/sql-snippets-query' -import { usePrefetchTables, useTablesQuery, type TablesData } from '@/data/tables/tables-query' +import { + useInfiniteTablesQuery, + usePrefetchTables, + type TablesData, +} from '@/data/tables/tables-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -import { useProtectedSchemas } from '@/hooks/useProtectedSchemas' import { useProfile } from '@/lib/profile' export function useSqlEditorGotoCommands(options?: CommandOptions) { @@ -294,34 +298,66 @@ export function useQueryTableCommands(options?: CommandOptions) { function TableSelector() { const router = useRouter() const { data: project } = useSelectedProjectQuery() - const { data: protectedSchemas } = useProtectedSchemas() + + const [filterString, setFilterString] = useState('') + const debouncedFilterString = useDebounce(filterString, 300) + + const [sentinelRef, sentinelEntry] = useIntersectionObserver({ + threshold: 0, + rootMargin: '200px 0px 200px 0px', + }) + const { data: tablesData, - isPending: isLoading, isError, isSuccess, - } = useTablesQuery({ + isPending: isLoading, + hasNextPage: hasNextTablesPage, + isFetchingNextPage: isFetchingNextTablesPage, + fetchNextPage: fetchNextTablesPage, + } = useInfiniteTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, includeColumns: true, + pageSize: 50, + nameFilter: debouncedFilterString, }) - const tables = useMemo(() => { - return tablesData?.filter((table) => !protectedSchemas.find((s) => s.name === table.schema)) - }, [tablesData, protectedSchemas]) + const tables = tablesData?.pages.flat() ?? [] + + useEffect(() => { + if ( + sentinelEntry?.isIntersecting && + hasNextTablesPage && + !isFetchingNextTablesPage && + isSuccess + ) { + fetchNextTablesPage() + } + }, [ + isSuccess, + sentinelEntry?.isIntersecting, + hasNextTablesPage, + isFetchingNextTablesPage, + fetchNextTablesPage, + ]) return ( - + - + - + {isLoading && } {isError && } {isSuccess && ( <> - - + {tables?.map((table) => ( ))} + {tables.length === 0 && debouncedFilterString && ( +

+ No tables found based on your search +

+ )} + {tables.length > 0 && ( + <> +
+
+
+ {isFetchingNextTablesPage ? ( + + Loading... + + ) : ( + + Total: {tables.length} {tables.length === 1 ? 'table' : 'tables'} + {hasNextTablesPage ? ' loaded' : ''} + + )} +
+
+ + )} )} diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts index 6e544e99dbe..f399e554541 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts @@ -129,7 +129,10 @@ export const getContextualInvalidationKeys = ({ 'auth/users': [authKeys.usersInfinite(ref)], 'auth/policies': [databasePoliciesKeys.list(ref)], 'database/functions': [databaseKeys.databaseFunctions(ref)], - 'database/tables': [tableKeys.list(ref, schema, true), tableKeys.list(ref, schema, false)], + 'database/tables': [ + tableKeys.list(ref, schema, { includeColumns: true }), + tableKeys.list(ref, schema, { includeColumns: false }), + ], 'database/triggers': [databaseTriggerKeys.list(ref)], 'database/types': [enumeratedTypesKeys.list(ref)], 'database/extensions': [databaseExtensionsKeys.list(ref)], diff --git a/apps/studio/data/tables/keys.ts b/apps/studio/data/tables/keys.ts index 51440a268cc..fc6f54875e7 100644 --- a/apps/studio/data/tables/keys.ts +++ b/apps/studio/data/tables/keys.ts @@ -1,7 +1,10 @@ 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), + list: ( + projectRef: string | undefined, + schema: string | undefined, + options?: { includeColumns?: boolean } + ) => ['projects', projectRef, 'tables', schema, options].filter(Boolean), infiniteListPrefix: (projectRef: string | undefined, schema?: string) => ['projects', projectRef, 'tables', 'infinite', schema].filter( (part) => part !== undefined && part !== null && part !== '' diff --git a/apps/studio/data/tables/table-create-mutation.ts b/apps/studio/data/tables/table-create-mutation.ts index 74151da942d..ffe9dab4693 100644 --- a/apps/studio/data/tables/table-create-mutation.ts +++ b/apps/studio/data/tables/table-create-mutation.ts @@ -52,10 +52,10 @@ export const useTableCreateMutation = ({ await Promise.all([ queryClient.invalidateQueries({ - queryKey: tableKeys.list(projectRef, payload.schema, true), + queryKey: tableKeys.list(projectRef, payload.schema, { includeColumns: true }), }), queryClient.invalidateQueries({ - queryKey: tableKeys.list(projectRef, payload.schema, false), + queryKey: tableKeys.list(projectRef, payload.schema, { includeColumns: false }), }), queryClient.invalidateQueries({ queryKey: privilegeKeys.tablePrivilegesList(projectRef), diff --git a/apps/studio/data/tables/tables-query.ts b/apps/studio/data/tables/tables-query.ts index 5add82825bd..7101ac5da7c 100644 --- a/apps/studio/data/tables/tables-query.ts +++ b/apps/studio/data/tables/tables-query.ts @@ -1,11 +1,10 @@ -import { getTablesPaginatedSql, type PGTable } from '@supabase/pg-meta' -import { DEFAULT_PLATFORM_APPLICATION_NAME } from '@supabase/pg-meta/src/constants' +import pgMeta, { getTablesPaginatedSql, type PGTable } from '@supabase/pg-meta' import { InfiniteData, useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query' import { sortBy } from 'lodash' import { useCallback } from 'react' +import { z } from 'zod' import { tableKeys } from './keys' -import { get, handleError } from '@/data/fetchers' import { executeSql } from '@/data/sql/execute-sql-mutation' import type { SafePostgresTable } from '@/lib/postgres-types' import type { ResponseError, UseCustomInfiniteQueryOptions, UseCustomQueryOptions } from '@/types' @@ -14,13 +13,14 @@ export type TablesVariables = { projectRef?: string connectionString?: string | null schema?: string - /** - * Defaults to false - */ + /** Defaults to false */ includeColumns?: boolean sortByProperty?: keyof PGTable } +const pgMetaTablesList = pgMeta.tables.list() +export type TablesData = z.infer + export async function getTables( { projectRef, @@ -31,55 +31,31 @@ export async function getTables( }: TablesVariables, signal?: AbortSignal ) { - if (!projectRef) { - throw new Error('projectRef is required') - } - - let headers = new Headers() - if (connectionString) headers.set('x-connection-encrypted', connectionString) - - let queryParams: Record = { - //include_columns is a string, even though it's true or false - include_columns: `${includeColumns}`, - } - if (schema) { - queryParams.included_schemas = schema - } - - const { data, error } = await get('/platform/pg-meta/{ref}/tables', { - params: { - header: { - 'x-connection-encrypted': connectionString!, - 'x-pg-application-name': DEFAULT_PLATFORM_APPLICATION_NAME, - }, - path: { ref: projectRef }, - query: queryParams as any, - }, - headers, - signal, - }) - - if (!Array.isArray(data) && error) handleError(error) + if (!projectRef) throw new Error('projectRef is required') + const sql = pgMeta.tables.list({ + includeColumns, + includedSchemas: schema ? [schema] : undefined, + }).sql + const queryKey = ['tables', schema] + const { result } = await executeSql({ projectRef, connectionString, sql, queryKey }, signal) // Sort the data if the sortByName option is true - if (Array.isArray(data) && sortByProperty) { - return sortBy(data, (t) => t[sortByProperty]) as SafePostgresTable[] + if (Array.isArray(result) && sortByProperty) { + return sortBy(result, (t) => t[sortByProperty]) as SafePostgresTable[] } - - return data as SafePostgresTable[] + return result as SafePostgresTable[] } -export type TablesData = Awaited> export type TablesError = ResponseError export const useTablesQuery = ( - { projectRef, connectionString, schema, includeColumns }: TablesVariables, + vars: TablesVariables, { enabled = true, ...options }: UseCustomQueryOptions = {} ) => { + const { projectRef, schema, includeColumns } = vars return useQuery({ - queryKey: tableKeys.list(projectRef, schema, includeColumns), - queryFn: ({ signal }) => - getTables({ projectRef, connectionString, schema, includeColumns }, signal), + queryKey: tableKeys.list(projectRef, schema, { includeColumns }), + queryFn: ({ signal }) => getTables(vars, signal), enabled: enabled && typeof projectRef !== 'undefined', ...options, }) @@ -98,7 +74,7 @@ export function useGetTables({ return useCallback( (schema?: TablesVariables['schema'], includeColumns?: TablesVariables['includeColumns']) => { return queryClient.fetchQuery({ - queryKey: tableKeys.list(projectRef, schema, includeColumns), + queryKey: tableKeys.list(projectRef, schema, { includeColumns }), queryFn: ({ signal }) => getTables({ projectRef, connectionString, schema, includeColumns }, signal), }) @@ -116,7 +92,7 @@ export function usePrefetchTables({ return useCallback( (schema?: TablesVariables['schema'], includeColumns?: TablesVariables['includeColumns']) => { return queryClient.prefetchQuery({ - queryKey: tableKeys.list(projectRef, schema, includeColumns), + queryKey: tableKeys.list(projectRef, schema, { includeColumns }), queryFn: ({ signal }) => getTables({ projectRef, connectionString, schema, includeColumns }, signal), }) @@ -176,7 +152,7 @@ export async function getTablesPage( return result as SafePostgresTable[] } -export const useInfiniteTablesQuery = >( +export const useInfiniteTablesQuery = >( { projectRef, connectionString, diff --git a/packages/pg-meta/src/sql/studio/database/tables-paginated.ts b/packages/pg-meta/src/sql/studio/database/tables-paginated.ts index d18dc71867d..0667bf2cdbe 100644 --- a/packages/pg-meta/src/sql/studio/database/tables-paginated.ts +++ b/packages/pg-meta/src/sql/studio/database/tables-paginated.ts @@ -37,7 +37,12 @@ export const getTablesPaginatedSql = ({ const schemaFilter = filter ? safeSql`and nc.nspname ${filter}` : safeSql`` const nameFilterClause = nameFilter && nameFilter.length > 0 - ? safeSql`and c.relname ilike ${literal(`%${escapeIlikeLiteral(nameFilter)}%`)}` + ? schema + ? safeSql`and c.relname ilike ${literal(`%${escapeIlikeLiteral(nameFilter)}%`)}` + : safeSql`and ( + c.relname ilike ${literal(`%${escapeIlikeLiteral(nameFilter)}%`)} + or nc.nspname ilike ${literal(`%${escapeIlikeLiteral(nameFilter)}%`)} + )` : safeSql`` const columnsCte = includeColumns diff --git a/packages/ui/src/components/shadcn/ui/command.tsx b/packages/ui/src/components/shadcn/ui/command.tsx index 170c6b245ea..b75f0d9b62c 100644 --- a/packages/ui/src/components/shadcn/ui/command.tsx +++ b/packages/ui/src/components/shadcn/ui/command.tsx @@ -90,7 +90,7 @@ const CommandList = React.forwardRef< >(({ className, ...props }, ref) => ( ))