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 (
)
}
function ErrorState() {
return (
)
}
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}`
}