import type { PGColumn } from '@supabase/pg-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' 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 { CodeBlock } from 'ui-patterns/CodeBlock' import type { CommandOptions } from 'ui-patterns/CommandMenu' import { Breadcrumb, CommandHeader, CommandMenuInput, CommandWrapper, escapeAttributeSelector, generateCommandClassNames, PageType, useCommandFilterState, useCommandMenuOpen, useRegisterCommands, useRegisterPage, useSetCommandMenuSize, useSetPage, } from 'ui-patterns/CommandMenu' 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 { 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) { let { ref } = useParams() ref ||= '_' useRegisterCommands( COMMAND_MENU_SECTIONS.NAVIGATE, [ { id: 'nav-sql-editor', name: 'SQL Editor', route: `/project/${ref}/sql`, defaultHidden: true, }, ], { ...options, deps: [ref] } ) } const SNIPPET_PAGE_NAME = 'Snippets' export function useSnippetCommands() { const { data: project } = useSelectedProjectQuery() const setPage = useSetPage() useRegisterPage( SNIPPET_PAGE_NAME, { type: PageType.Component, component: () => , }, { enabled: !!project } ) useRegisterCommands( COMMAND_MENU_SECTIONS.SQL, [ { id: 'run-snippet', name: 'Run snippet...', icon: () => , action: () => setPage(SNIPPET_PAGE_NAME), }, ], { enabled: !!project, orderSection: orderCommandSectionsByPriority, sectionMeta: { priority: 3 }, } ) } function RunSnippetPage() { const { ref } = useParams() const { data: snippetPages, isPending: isLoading, isError, isSuccess, } = useSqlSnippetsQuery({ projectRef: ref, }) const snippets = snippetPages?.pages.flatMap((page) => page.contents) const { profile } = useProfile() const { can: canCreateSQLSnippet } = useAsyncCheckPermissions( PermissionAction.CREATE, 'user_content', { resource: { type: 'sql', owner_id: profile?.id }, subject: { id: profile?.id }, } ) useSetCommandMenuSize('xlarge') return ( {isLoading && } {isError && } {isSuccess && (!snippets || snippets.length === 0) && ( )} {isSuccess && !!snippets && snippets.length > 0 && ( )} ) } function LoadingState() { return (

Loading...

) } function ErrorState() { return (

Couldn't load snippets

) } function EmptyState({ projectRef, canCreateNew, }: { projectRef: string | undefined canCreateNew: boolean }) { const router = useRouter() return (

No snippets found.

router.push(`/project/${projectRef ?? '_'}/sql/new`)} > {canCreateNew ? 'Create new snippet' : 'Run new SQL'}
) } function SnippetSelector({ projectRef, snippets, canCreateNew, }: { projectRef: string | undefined snippets: Array | undefined canCreateNew: boolean }) { const router = useRouter() const selectedValue = useCommandFilterState((state) => state.value) const selectedSnippet = snippets?.find((snippet) => snippetValue(snippet) === selectedValue) const isSQLSnippet = selectedSnippet?.type === 'sql' return (
{!!snippets && snippets.length > 0 && ( {snippets.map((snippet) => ( void router.push(`/project/${projectRef ?? '_'}/sql/${snippet.id}`)} > {snippet.name} ))} )} {canCreateNew && (

router.push(`/project/${projectRef ?? '_'}/sql/new`)} forceMount={true} > Create new snippet
)}
) } function snippetValue(snippet: SqlSnippet) { if (snippet.type !== 'sql') return '' return escapeAttributeSelector( `${snippet.id}-${snippet.name}-${snippet?.content?.unchecked_sql.slice(0, 30)}` ).toLowerCase() } const QUERY_TABLE_PAGE_NAME = 'Query a table' export function useQueryTableCommands(options?: CommandOptions) { const { data: project } = useSelectedProjectQuery() const setPage = useSetPage() const commandMenuOpen = useCommandMenuOpen() const commandMenuPreviouslyOpen = useRef(commandMenuOpen) const commandMenuJustOpened = commandMenuOpen && !commandMenuPreviouslyOpen.current commandMenuPreviouslyOpen.current = commandMenuOpen const prefetchTables = usePrefetchTables({ projectRef: project?.ref, connectionString: project?.connectionString, }) useEffect(() => { if (project && commandMenuJustOpened) { prefetchTables(undefined, true) } }, [project, prefetchTables, commandMenuJustOpened]) useRegisterPage( QUERY_TABLE_PAGE_NAME, { type: PageType.Component, component: TableSelector, }, { enabled: !!project } ) useRegisterCommands( COMMAND_MENU_SECTIONS.SQL, [ { id: 'query-table', name: 'Query a table...', icon: () => , action: () => setPage(QUERY_TABLE_PAGE_NAME), }, ], { ...options, enabled: (options?.enabled ?? true) && !!project } ) } function TableSelector() { const router = useRouter() const { data: project } = useSelectedProjectQuery() const { data: protectedSchemas } = useProtectedSchemas() const { data: tablesData, isPending: isLoading, isError, isSuccess, } = useTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, includeColumns: true, }) const tables = useMemo(() => { return tablesData?.filter((table) => !protectedSchemas.find((s) => s.name === table.schema)) }, [tablesData, protectedSchemas]) return ( {isLoading && } {isError && } {isSuccess && ( <> {tables?.map((table) => ( { router.push( `/project/${project?.ref ?? '_'}/sql/new?content=${encodeURIComponent(generateSelectStatement(table))}` ) }} > {`${table.schema}.${table.name}`} ))} )} ) } function generateSelectStatement(table: TablesData[number] & { columns?: Array }) { return ` select ${ !table.columns ? '*' : ` ${table.columns.map((column) => `\t${column.name}`).join(',\n')}` } from ${formatTableIdentifier(table)} -- where -- order by -- limit ; `.trim() } // Not a perfectly spec-compliant regex , since Postgres also allows non-Latin // letters and letters with diacritical marks, but quoting them defensively // is easier than writing the regex. ¯\_(ツ)_/¯ const VALID_UNQUOTED_IDENTIFIER_REGEX = /^[a-z_][a-z0-9_$]*$/ function formatTableIdentifier(table: TablesData[number]) { const schema = VALID_UNQUOTED_IDENTIFIER_REGEX.test(table.schema) ? table.schema : `"${table.schema}"` const tableName = VALID_UNQUOTED_IDENTIFIER_REGEX.test(table.name) ? table.name : `"${table.name}"` return `${schema}.${tableName}` }