chore: pg-meta functions (#26881)

* start pg-meta functions

* pg meta create function

* pg meta database functions update and delete

* fix ts

* update execute sql error type

* update execute sql error type

* remove duplicate database functions query
This commit is contained in:
Alaister Young authored and Chris Gwilliams committed 2024-06-04 17:55:07 +03:00
1 parent 2310b9a63b
commit b4b67d22f3
38 files changed
+608 -242

No files matched your search

@@ -71,9 +71,11 @@ const FunctionSelector = ({
<AlertTitle_Shadcn_ className="text-xs text-amber-900">
Failed to load functions
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="text-xs mb-2">
Error: {error.message}
</AlertDescription_Shadcn_>
<Button type="default" size="tiny" onClick={() => refetch()}>
Reload functions
</Button>
@@ -1,5 +1,4 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PostgresFunction } from '@supabase/postgres-meta'
import { isEmpty, isNull, keyBy, mapValues, partition } from 'lodash'
import { Plus, Trash } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
@@ -12,6 +11,7 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte
import SchemaSelector from 'components/ui/SchemaSelector'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useDatabaseFunctionCreateMutation } from 'data/database-functions/database-functions-create-mutation'
import { DatabaseFunction } from 'data/database-functions/database-functions-query'
import { useDatabaseFunctionUpdateMutation } from 'data/database-functions/database-functions-update-mutation'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import type { FormSchema } from 'types'
@@ -50,7 +50,7 @@ import { FunctionEditor } from './FunctionEditor'
const FORM_ID = 'create-function-sidepanel'
interface CreateFunctionProps {
func?: PostgresFunction
func?: DatabaseFunction
visible: boolean
setVisible: (value: boolean) => void
}
@@ -103,7 +103,7 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
if (isEditing) {
updateDatabaseFunction(
{
id: func.id,
func,
projectRef: project.ref,
connectionString: project.connectionString,
payload,
@@ -3,16 +3,17 @@ import toast from 'react-hot-toast'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
import { useDatabaseFunctionDeleteMutation } from 'data/database-functions/database-functions-delete-mutation'
import { DatabaseFunction } from 'data/database-functions/database-functions-query'
interface DeleteFunctionProps {
func?: any
func?: DatabaseFunction
visible: boolean
setVisible: (value: boolean) => void
}
const DeleteFunction = ({ func, visible, setVisible }: DeleteFunctionProps) => {
const { project } = useProjectContext()
const { id, name, schema } = func ?? {}
const { name, schema } = func ?? {}
const { mutate: deleteDatabaseFunction, isLoading } = useDatabaseFunctionDeleteMutation({
onSuccess: () => {
@@ -22,11 +23,11 @@ const DeleteFunction = ({ func, visible, setVisible }: DeleteFunctionProps) => {
})
async function handleDelete() {
if (!id) return console.error('Function ID is require')
if (!func) return console.error('Function is required')
if (!project) return console.error('Project is required')
deleteDatabaseFunction({
id,
func,
projectRef: project.ref,
connectionString: project.connectionString,
})
@@ -43,7 +44,7 @@ const DeleteFunction = ({ func, visible, setVisible }: DeleteFunctionProps) => {
loading={isLoading}
confirmLabel={`Delete function ${name}`}
confirmPlaceholder="Type in name of function"
confirmString={name}
confirmString={name ?? 'Unknown'}
text={
<>
<span>This will delete the function</span>{' '}
@@ -86,7 +86,7 @@ const FunctionsList = ({
})
if (isLoading) return <GenericSkeletonLoader />
if (isError) <AlertError error={error} subject="Failed to retrieve database functions" />
if (isError) return <AlertError error={error} subject="Failed to retrieve database functions" />
return (
<>
@@ -1,8 +1,8 @@
import type { RefObject } from 'react'
import BackwardIterator from './BackwardIterator'
import type { DatabaseFunction } from 'data/database-functions/database-functions-query'
import type { Schema } from 'data/database/schemas-query'
import type { TableColumn } from 'data/database/table-columns-query'
import type { DatabaseFunction } from 'data/database/functions-query'
import type { RefObject } from 'react'
import BackwardIterator from './BackwardIterator'
// [Joshen] Needs to be fixed
@@ -200,8 +200,7 @@ function defaultScenarioSuggestions(monaco: any, pgInfoRef: RefObject<any>) {
items.push({
label: x.name,
kind: monaco.languages.CompletionItemKind.Function,
detail: x.result_type,
documentation: x.description,
detail: x.return_type,
insertText: x.name,
})
})
@@ -1,5 +1,6 @@
import { RefObject } from 'react'
import BackwardIterator from './BackwardIterator'
import type { DatabaseFunction } from 'data/database-functions/database-functions-query'
// [Joshen] Needs to be fixed
@@ -17,8 +18,8 @@ export default function getPgsqlSignatureHelpProvider(monaco: any, pgInfoRef: Re
let ident = iterator.readIdent()
if (!ident || ident.match(/^\".*?\"$/)) return null
let fn = pgInfoRef.current.functions.find(
(f: any) => f.name.toLocaleLowerCase() === ident.toLocaleLowerCase()
let fn = (pgInfoRef.current.functions as DatabaseFunction[]).find(
(f) => f.name.toLocaleLowerCase() === ident.toLocaleLowerCase()
)
if (!fn) return null
if (!fn.args || fn.args.length < paramCount) return null
@@ -27,10 +28,9 @@ export default function getPgsqlSignatureHelpProvider(monaco: any, pgInfoRef: Re
const activeParameter = Math.min(paramCount, fn.args.length - 1)
const signatures = []
signatures.push({
label: `${fn.name}( ${fn.args.join(' , ')} )`,
documentation: fn.description,
parameters: fn.args.map((v: any) => {
return { label: v }
label: `${fn.name}(${fn.argument_types})`,
parameters: fn.args.map((v) => {
return { label: v.name }
}),
})
@@ -1,24 +1,16 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { z } from 'zod'
import { handleError, post } from 'data/fetchers'
import type { Dictionary, ResponseError } from 'types'
import { databaseFunctionsKeys } from './keys'
import pgMeta from '@supabase/pg-meta'
import { executeSql } from 'data/sql/execute-sql-query'
import type { ResponseError } from 'types'
import { sqlKeys } from 'data/sql/keys'
export type DatabaseFunctionCreateVariables = {
projectRef: string
connectionString?: string
payload: {
name: string
schema: string
args: string[]
behavior: string // 'VOLATILE' | 'STABLE' | 'IMMUTABLE'
definition: string
language: string
return_type: string
security_definer: boolean
config_params?: Dictionary<string>
}
payload: z.infer<typeof pgMeta.functions.pgFunctionCreateZod>
}
export async function createDatabaseFunction({
@@ -26,20 +18,16 @@ export async function createDatabaseFunction({
connectionString,
payload,
}: DatabaseFunctionCreateVariables) {
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const { sql, zod } = pgMeta.functions.create(payload)
const { data, error } = await post('/platform/pg-meta/{ref}/functions', {
params: {
path: { ref: projectRef },
},
// @ts-ignore API codegen is typed wrongly, i suspect its using the body for edge functions instead
body: payload,
headers,
const { result } = await executeSql({
projectRef,
connectionString,
sql,
queryKey: ['functions', 'create'],
})
if (error) handleError(error)
return data
return result as z.infer<typeof zod>
}
type DatabaseFunctionCreateData = Awaited<ReturnType<typeof createDatabaseFunction>>
@@ -59,7 +47,7 @@ export const useDatabaseFunctionCreateMutation = ({
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(databaseFunctionsKeys.list(projectRef))
await queryClient.invalidateQueries(sqlKeys.query(projectRef, ['functions-list']))
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
@@ -1,35 +1,34 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { z } from 'zod'
import { del, handleError } from 'data/fetchers'
import pgMeta from '@supabase/pg-meta'
import { executeSql } from 'data/sql/execute-sql-query'
import { sqlKeys } from 'data/sql/keys'
import type { ResponseError } from 'types'
import { databaseFunctionsKeys } from './keys'
import { DatabaseFunction } from './database-functions-query'
export type DatabaseFunctionDeleteVariables = {
projectRef: string
connectionString?: string
id: number
func: DatabaseFunction
}
export async function deleteDatabaseFunction({
projectRef,
connectionString,
id,
func,
}: DatabaseFunctionDeleteVariables) {
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const { sql, zod } = pgMeta.functions.remove(func)
const { data, error } = await del('/platform/pg-meta/{ref}/functions', {
params: {
header: { 'x-connection-encrypted': connectionString! },
path: { ref: projectRef },
query: { id },
},
headers,
const { result } = await executeSql({
projectRef,
connectionString,
sql,
queryKey: ['functions', 'delete', func.id.toString()],
})
if (error) handleError(error)
return data
return result as z.infer<typeof zod>
}
type DatabaseFunctionDeleteData = Awaited<ReturnType<typeof deleteDatabaseFunction>>
@@ -49,7 +48,7 @@ export const useDatabaseFunctionDeleteMutation = ({
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(databaseFunctionsKeys.list(projectRef))
await queryClient.invalidateQueries(sqlKeys.query(projectRef, ['functions-list']))
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
@@ -1,52 +1,40 @@
import { UseQueryOptions, useQuery } from '@tanstack/react-query'
import type { components } from 'data/api'
import { get, handleError } from 'data/fetchers'
import pgMeta from '@supabase/pg-meta'
import { UseQueryOptions } from '@tanstack/react-query'
import type { ExecuteSqlData, ExecuteSqlError } from 'data/sql/execute-sql-query'
import { useExecuteSqlQuery } from 'data/sql/execute-sql-query'
import type { ResponseError } from 'types'
import { databaseFunctionsKeys } from './keys'
import { z } from 'zod'
export type DatabaseFunctionsVariables = {
projectRef?: string
connectionString?: string
}
export type DatabaseFunction = components['schemas']['PostgresFunction']
export type DatabaseFunction = z.infer<typeof pgMeta.functions.pgFunctionZod>
export async function getDatabaseFunctions(
{ projectRef, connectionString }: DatabaseFunctionsVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
const pgMetaFunctionsList = pgMeta.functions.list()
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const { data, error } = await get('/platform/pg-meta/{ref}/functions', {
// @ts-ignore [Joshen] Temp, seems like API codegen is wrong
params: { path: { ref: projectRef } },
headers,
signal,
})
if (error) handleError(error)
// [Joshen] API codegen is wrong, its matching Edge functions type to database functions
return data as unknown as DatabaseFunction[]
}
export type DatabaseFunctionsData = Awaited<ReturnType<typeof getDatabaseFunctions>>
export type DatabaseFunctionsData = z.infer<typeof pgMetaFunctionsList.zod>
export type DatabaseFunctionsError = ResponseError
export const useDatabaseFunctionsQuery = <TData = DatabaseFunctionsData>(
export const useDatabaseFunctionsQuery = <
TData extends DatabaseFunctionsData = DatabaseFunctionsData,
>(
{ projectRef, connectionString }: DatabaseFunctionsVariables,
{
enabled = true,
...options
}: UseQueryOptions<DatabaseFunctionsData, DatabaseFunctionsError, TData> = {}
) =>
useQuery<DatabaseFunctionsData, DatabaseFunctionsError, TData>(
databaseFunctionsKeys.list(projectRef),
({ signal }) => getDatabaseFunctions({ projectRef, connectionString }, signal),
options: UseQueryOptions<ExecuteSqlData, ExecuteSqlError, TData> = {}
) => {
return useExecuteSqlQuery(
{
enabled: enabled && typeof projectRef !== 'undefined',
projectRef,
connectionString,
sql: pgMetaFunctionsList.sql,
queryKey: ['functions-list'],
},
{
select(data) {
return (data as any)?.result ?? []
},
...options,
}
)
}
@@ -1,39 +1,36 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { z } from 'zod'
import type { components } from 'data/api'
import { handleError, patch } from 'data/fetchers'
import pgMeta from '@supabase/pg-meta'
import { executeSql } from 'data/sql/execute-sql-query'
import { sqlKeys } from 'data/sql/keys'
import type { ResponseError } from 'types'
import { databaseFunctionsKeys } from './keys'
import type { DatabaseFunction } from './database-functions-query'
export type DatabaseFunctionUpdateVariables = {
projectRef: string
connectionString?: string
id: number
payload: components['schemas']['UpdateFunctionBody']
func: DatabaseFunction
payload: z.infer<typeof pgMeta.functions.pgFunctionCreateZod>
}
export async function updateDatabaseFunction({
projectRef,
connectionString,
id,
func,
payload,
}: DatabaseFunctionUpdateVariables) {
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const { sql, zod } = pgMeta.functions.update(func, payload)
const { data, error } = await patch('/platform/pg-meta/{ref}/functions', {
params: {
header: { 'x-connection-encrypted': connectionString! },
path: { ref: projectRef },
query: { id },
},
body: payload,
headers,
const { result } = await executeSql({
projectRef,
connectionString,
sql,
queryKey: ['functions', 'update', func.id.toString()],
})
if (error) handleError(error)
return data
return result as z.infer<typeof zod>
}
type DatabaseFunctionUpdateData = Awaited<ReturnType<typeof updateDatabaseFunction>>
@@ -53,7 +50,7 @@ export const useDatabaseFunctionUpdateMutation = ({
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(databaseFunctionsKeys.list(projectRef))
await queryClient.invalidateQueries(sqlKeys.query(projectRef, ['functions-list']))
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
@@ -1,3 +0,0 @@
export const databaseFunctionsKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'database-functions'] as const,
}
@@ -2,7 +2,7 @@ import pgMeta from '@supabase/pg-meta'
import { QueryClient, UseQueryOptions } from '@tanstack/react-query'
import { z } from 'zod'
import { ExecuteSqlData, useExecuteSqlQuery } from 'data/sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from 'data/sql/execute-sql-query'
import { sqlKeys } from 'data/sql/keys'
export type DatabaseRolesVariables = {
@@ -15,7 +15,7 @@ export type PgRole = z.infer<typeof pgMeta.roles.zod>
const pgMetaRolesList = pgMeta.roles.list()
export type DatabaseRolesData = z.infer<typeof pgMetaRolesList.zod>
export type DatabaseRolesError = unknown
export type DatabaseRolesError = ExecuteSqlError
export const useDatabaseRolesQuery = <TData = DatabaseRolesData>(
{ projectRef, connectionString }: DatabaseRolesVariables,
@@ -1,5 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
type GetTableConstraintsVariables = {
schema?: string
@@ -45,7 +45,7 @@ export type TableConstraintsVariables = GetTableConstraintsVariables & {
}
export type TableConstraintsData = Constraint[]
export type TableConstraintsError = unknown
export type TableConstraintsError = ExecuteSqlError
export const useTableConstraintsQuery = <TData extends TableConstraintsData = TableConstraintsData>(
{ projectRef, connectionString, schema, table }: TableConstraintsVariables,
@@ -1,5 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
export const getDatabaseSizeQuery = () => {
const sql = /* SQL */ `
@@ -15,7 +15,7 @@ export type DatabaseSizeVariables = {
}
export type DatabaseSizeData = { result: { db_size: number }[] }
export type DatabaseSizeError = unknown
export type DatabaseSizeError = ExecuteSqlError
export const useDatabaseSizeQuery = <TData extends DatabaseSizeData = DatabaseSizeData>(
{ projectRef, connectionString }: DatabaseSizeVariables,
@@ -1,5 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { CREATE_PG_GET_TABLEDEF_SQL } from './database-query-constants'
export const getEntityDefinitionsQuery = (limit = 100) => {
@@ -82,7 +82,7 @@ export type EntityDefinitionsVariables = {
type EntityDefinition = { id: number; sql: string }
export type EntityDefinitionsData = EntityDefinition[]
export type EntityDefinitionsError = unknown
export type EntityDefinitionsError = ExecuteSqlError
export const useEntityDefinitionsQuery = <
TData extends EntityDefinitionsData = EntityDefinitionsData,
@@ -1,5 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
type GetForeignKeyConstraintsVariables = {
schema?: string
@@ -94,7 +94,7 @@ export type ForeignKeyConstraintsVariables = GetForeignKeyConstraintsVariables &
}
export type ForeignKeyConstraintsData = ForeignKeyConstraint[]
export type ForeignKeyConstraintsError = unknown
export type ForeignKeyConstraintsError = ExecuteSqlError
export const useForeignKeyConstraintsQuery = <
TData extends ForeignKeyConstraintsData = ForeignKeyConstraintsData,
@@ -1,71 +0,0 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
export type DatabaseFunction = {
schema: string
name: string
description: string
result_type: any
argument_types: any
args?: string[]
}
export const getFunctionsQuery = () => {
const sql = /* SQL */ `
SELECT n.nspname as "schema",
p.proname as "name",
d.description,
pg_catalog.pg_get_function_result(p.oid) as "result_type",
pg_catalog.pg_get_function_arguments(p.oid) as "argument_types",
CASE
WHEN p.prokind = 'a' THEN 'agg'
WHEN p.prokind = 'w' THEN 'window'
WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN 'trigger'
ELSE 'normal'
END as "type"
FROM pg_catalog.pg_proc p
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
LEFT JOIN pg_catalog.pg_description d ON p.oid = d.objoid
WHERE n.nspname = 'public'
ORDER BY 1, 2, 4;
`.trim()
return sql
}
export type FunctionsVariables = {
projectRef?: string
connectionString?: string
}
export type FunctionsData = { result: DatabaseFunction[] }
export type FunctionsError = unknown
export const useFunctionsQuery = <TData extends FunctionsData = FunctionsData>(
{ projectRef, connectionString }: FunctionsVariables,
options: UseQueryOptions<ExecuteSqlData, FunctionsError, TData> = {}
) => {
return useExecuteSqlQuery(
{
projectRef,
connectionString,
sql: getFunctionsQuery(),
queryKey: ['functions'],
},
{
select: (data) => {
return {
result: data.result.map((x: DatabaseFunction) => {
const args = x.argument_types
.split(',')
.filter((a: any) => a)
.map((a: any) => a.trim())
return { ...x, args }
}),
} as any
},
...options,
}
)
}
+2 -3
View File
@@ -1,6 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import type { ResponseError } from 'types'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
// [Joshen] Future refactor to move to pg-meta once available
@@ -35,7 +34,7 @@ export type IndexesVariables = getIndexesDefinition & {
}
export type IndexesData = { result: DatabaseIndex[] }
export type IndexesError = unknown
export type IndexesError = ExecuteSqlError
export const useIndexesQuery = <TData extends IndexesData = IndexesData>(
{ schema, projectRef, connectionString }: IndexesVariables,
+2 -2
View File
@@ -1,5 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
export type DatabaseKeyword = { word: string }
@@ -17,7 +17,7 @@ export type KeywordsVariables = {
}
export type KeywordsData = { result: string[] }
export type KeywordsError = unknown
export type KeywordsError = ExecuteSqlError
export const useKeywordsQuery = <TData extends KeywordsData = KeywordsData>(
{ projectRef, connectionString }: KeywordsVariables,
@@ -1,5 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
export const getMaxConnectionsQuery = () => {
const sql = /* SQL */ `show max_connections`
@@ -15,7 +15,7 @@ export type MaxConnectionsVariables = {
}
export type MaxConnectionsData = { maxConnections: number }
export type MaxConnectionsError = unknown
export type MaxConnectionsError = ExecuteSqlError
export const useMaxConnectionsQuery = <TData extends MaxConnectionsData = MaxConnectionsData>(
{ projectRef, connectionString }: MaxConnectionsVariables,
@@ -1,5 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
export type DatabaseMigration = {
version: string
@@ -24,7 +24,7 @@ export type MigrationsVariables = {
}
export type MigrationsData = { result: DatabaseMigration[] }
export type MigrationsError = unknown
export type MigrationsError = ExecuteSqlError
export const useMigrationsQuery = <TData extends MigrationsData = MigrationsData>(
{ projectRef, connectionString }: MigrationsVariables,
@@ -36,7 +36,7 @@ export const useMigrationsQuery = <TData extends MigrationsData = MigrationsData
connectionString,
sql: getMigrationsQuery(),
queryKey: ['migrations'],
handleError: (error: { code: number; message: string; requestId: string }) => {
handleError: (error) => {
if (
error.message.includes('relation "supabase_migrations.schema_migrations" does not exist')
) {
+2 -2
View File
@@ -2,7 +2,7 @@ import pgMeta from '@supabase/pg-meta'
import { QueryClient, UseQueryOptions } from '@tanstack/react-query'
import { z } from 'zod'
import { ExecuteSqlData, useExecuteSqlQuery } from 'data/sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from 'data/sql/execute-sql-query'
import { sqlKeys } from 'data/sql/keys'
export type SchemasVariables = {
@@ -15,7 +15,7 @@ export type Schema = z.infer<typeof pgMeta.schemas.zod>
const pgMetaSchemasList = pgMeta.schemas.list()
export type SchemasData = z.infer<typeof pgMetaSchemasList.zod>
export type SchemasError = unknown
export type SchemasError = ExecuteSqlError
export const useSchemasQuery = <TData = SchemasData>(
{ projectRef, connectionString }: SchemasVariables,
@@ -1,5 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
export type TableColumn = {
schemaname: string
@@ -92,7 +92,7 @@ export type TableColumnsVariables = {
}
export type TableColumnsData = { result: TableColumn[] }
export type TableColumnsError = unknown
export type TableColumnsError = ExecuteSqlError
export const useTableColumnsQuery = <TData extends TableColumnsData = TableColumnsData>(
{ projectRef, connectionString, table, schema }: TableColumnsVariables,
@@ -1,5 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { CREATE_PG_GET_TABLEDEF_SQL } from './database-query-constants'
type GetTableDefinition = {
@@ -29,7 +29,7 @@ export type TableDefinitionVariables = GetTableDefinition & {
}
export type TableDefinitionData = string
export type TableDefinitionError = unknown
export type TableDefinitionError = ExecuteSqlError
export const useTableDefinitionQuery = <TData extends TableDefinitionData = TableDefinitionData>(
{ projectRef, connectionString, schema, name }: TableDefinitionVariables,
@@ -1,5 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
type GetViewDefinition = {
schema?: string
@@ -20,7 +20,7 @@ export type ViewDefinitionVariables = GetViewDefinition & {
}
export type ViewDefinitionData = string
export type ViewDefinitionError = unknown
export type ViewDefinitionError = ExecuteSqlError
export const useViewDefinitionQuery = <TData extends ViewDefinitionData = ViewDefinitionData>(
{ projectRef, connectionString, schema, name }: ViewDefinitionVariables,
@@ -1,5 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ENTITY_TYPE } from './entity-type-constants'
type EntityTypeArgs = {
@@ -53,7 +53,7 @@ export type EntityTypeVariables = EntityTypeArgs & {
}
export type EntityTypeData = EntityType
export type EntityTypeError = unknown
export type EntityTypeError = ExecuteSqlError
export const useEntityTypeQuery = <TData extends EntityTypeData = EntityTypeData>(
{ projectRef, connectionString, id, ...args }: EntityTypeVariables,
+2 -2
View File
@@ -1,5 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
export const getFDWsSql = () => {
const sql = /* SQL */ `
@@ -72,7 +72,7 @@ export type FDWsVariables = {
}
export type FDWsData = FDWsResponse
export type FDWsError = unknown
export type FDWsError = ExecuteSqlError
export const useFDWsQuery = <TData extends FDWsData = FDWsData>(
{ projectRef, connectionString }: FDWsVariables,
@@ -1,5 +1,5 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
export const replicationLagQuery = () => {
const sql = /* SQL */ `
@@ -21,7 +21,7 @@ export type ReplicationLagVariables = {
}
export type ReplicationLagData = number
export type ReplicationLagError = unknown
export type ReplicationLagError = ExecuteSqlError
export const useReplicationLagQuery = <TData extends ReplicationLagData = ReplicationLagData>(
{ projectRef, connectionString, id }: ReplicationLagVariables,
+5 -6
View File
@@ -1,20 +1,19 @@
import { QueryClient, QueryKey, useQuery, UseQueryOptions } from '@tanstack/react-query'
import { post, handleError as handleErrorFetchers } from 'data/fetchers'
import { handleError as handleErrorFetchers, post } from 'data/fetchers'
import {
ROLE_IMPERSONATION_NO_RESULTS,
ROLE_IMPERSONATION_SQL_LINE_COUNT,
} from 'lib/role-impersonation'
import type { ResponseError } from 'types'
import { sqlKeys } from './keys'
export type Error = { code: number; message: string; requestId: string }
export type ExecuteSqlVariables = {
projectRef?: string
connectionString?: string
sql: string
queryKey?: QueryKey
handleError?: (error: { code: number; message: string; requestId: string }) => any
handleError?: (error: ResponseError) => { result: any }
isRoleImpersonationEnabled?: boolean
}
@@ -85,7 +84,7 @@ export async function executeSql(
}
if (handleError !== undefined) return handleError(error as any)
else handleErrorFetchers(error as any)
else handleErrorFetchers(error)
}
if (
@@ -100,7 +99,7 @@ export async function executeSql(
}
export type ExecuteSqlData = Awaited<ReturnType<typeof executeSql>>
export type ExecuteSqlError = unknown
export type ExecuteSqlError = ResponseError
export const useExecuteSqlQuery = <TData = ExecuteSqlData>(
{
@@ -3,7 +3,7 @@ import type { QueryKey, UseQueryOptions } from '@tanstack/react-query'
import { Filter, Query, SupaTable } from 'components/grid'
import { ImpersonationRole, wrapWithRoleImpersonation } from 'lib/role-impersonation'
import { useIsRoleImpersonationEnabled } from 'state/role-impersonation-state'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { formatFilterValue } from './utils'
type GetTableRowsCountArgs = {
@@ -43,7 +43,7 @@ export type TableRowsCountVariables = GetTableRowsCountArgs & {
}
export type TableRowsCountData = TableRowsCount
export type TableRowsCountError = unknown
export type TableRowsCountError = ExecuteSqlError
export const useTableRowsCountQuery = <TData extends TableRowsCountData = TableRowsCountData>(
{
@@ -13,7 +13,12 @@ import {
wrapWithRoleImpersonation,
} from 'lib/role-impersonation'
import { useIsRoleImpersonationEnabled } from 'state/role-impersonation-state'
import { ExecuteSqlData, executeSql, useExecuteSqlQuery } from '../sql/execute-sql-query'
import {
ExecuteSqlData,
ExecuteSqlError,
executeSql,
useExecuteSqlQuery,
} from '../sql/execute-sql-query'
import { getPagination } from '../utils/pagination'
import { formatFilterValue } from './utils'
@@ -155,7 +160,7 @@ export type TableRowsVariables = GetTableRowsArgs & {
}
export type TableRowsData = TableRows
export type TableRowsError = unknown
export type TableRowsError = ExecuteSqlError
export const useTableRowsQuery = <TData extends TableRowsData = TableRowsData>(
{ projectRef, connectionString, queryKey, table, impersonatedRole, ...args }: TableRowsVariables,
@@ -1,7 +1,7 @@
import { UseQueryOptions } from '@tanstack/react-query'
import { Query } from 'components/grid/query/Query'
import type { VaultSecret } from 'types'
import { ExecuteSqlData, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { vaultSecretsKeys } from './keys'
export const getVaultSecretsQuery = () => {
@@ -19,7 +19,7 @@ export type VaultSecretsVariables = {
}
export type VaultSecretsData = VaultSecret[]
export type VaultSecretsError = unknown
export type VaultSecretsError = ExecuteSqlError
export const useVaultSecretsQuery = <TData extends VaultSecretsData = VaultSecretsData>(
{ projectRef, connectionString }: VaultSecretsVariables,
@@ -1,4 +1,3 @@
import { PostgresFunction } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useState } from 'react'
@@ -8,11 +7,12 @@ import { DatabaseLayout } from 'components/layouts'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import { FormHeader } from 'components/ui/Forms'
import NoPermission from 'components/ui/NoPermission'
import { DatabaseFunction } from 'data/database-functions/database-functions-query'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks'
import type { NextPageWithLayout } from 'types'
const FunctionsPage: NextPageWithLayout = () => {
const [selectedFunction, setSelectedFunction] = useState<PostgresFunction | undefined>()
const [selectedFunction, setSelectedFunction] = useState<DatabaseFunction | undefined>()
const [showCreateFunctionForm, setShowCreateFunctionForm] = useState(false)
const [showDeleteFunctionForm, setShowDeleteFunctionForm] = useState(false)
+3 -3
View File
@@ -3,7 +3,7 @@ import { useParams } from 'common'
import { useRouter } from 'next/router'
import { useEffect, useRef } from 'react'
import { useFunctionsQuery } from 'data/database/functions-query'
import { useDatabaseFunctionsQuery } from 'data/database-functions/database-functions-query'
import { useKeywordsQuery } from 'data/database/keywords-query'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useTableColumnsQuery } from 'data/database/table-columns-query'
@@ -59,7 +59,7 @@ const SqlEditor: NextPageWithLayout = () => {
},
{ enabled: intellisenseEnabled }
)
const { data: functions, isSuccess: isFunctionsSuccess } = useFunctionsQuery(
const { data: functions, isSuccess: isFunctionsSuccess } = useDatabaseFunctionsQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
@@ -99,7 +99,7 @@ const SqlEditor: NextPageWithLayout = () => {
pgInfoRef.current.tableColumns = tableColumns?.result
pgInfoRef.current.schemas = schemas
pgInfoRef.current.keywords = keywords?.result
pgInfoRef.current.functions = functions?.result
pgInfoRef.current.functions = functions
}
useEffect(() => {
+13
View File
@@ -0,0 +1,13 @@
import { literal } from 'pg-format'
export function filterByList(include?: string[], exclude?: string[], defaultExclude?: string[]) {
if (defaultExclude) {
exclude = defaultExclude.concat(exclude ?? [])
}
if (include?.length) {
return `IN (${include.map(literal).join(',')})`
} else if (exclude?.length) {
return `NOT IN (${exclude.map(literal).join(',')})`
}
return ''
}
+2
View File
@@ -1,7 +1,9 @@
import roles from './pg-meta-roles'
import schemas from './pg-meta-schemas'
import * as functions from './pg-meta-functions'
export default {
roles,
schemas,
functions,
}
+339
View File
@@ -0,0 +1,339 @@
import { ident, literal } from 'pg-format'
import { z } from 'zod'
import { DEFAULT_SYSTEM_SCHEMAS } from './constants'
import { filterByList } from './helpers'
import { FUNCTIONS_SQL } from './sql/functions'
export const pgFunctionZod = z.object({
id: z.number(),
schema: z.string(),
name: z.string(),
language: z.string(),
definition: z.string(),
complete_statement: z.string(),
args: z.array(
z.object({
mode: z.union([
z.literal('in'),
z.literal('out'),
z.literal('inout'),
z.literal('variadic'),
z.literal('table'),
]),
name: z.string(),
type_id: z.number(),
has_default: z.boolean(),
})
),
argument_types: z.string(),
identity_argument_types: z.string(),
return_type_id: z.number(),
return_type: z.string(),
return_type_relation_id: z.union([z.number(), z.null()]),
is_set_returning_function: z.boolean(),
behavior: z.union([z.literal('IMMUTABLE'), z.literal('STABLE'), z.literal('VOLATILE')]),
security_definer: z.boolean(),
config_params: z.union([z.record(z.string(), z.string()), z.null()]),
})
export const pgFunctionArrayZod = z.array(pgFunctionZod)
export const pgFunctionOptionalZod = z.optional(pgFunctionZod)
export type PGFunction = z.infer<typeof pgFunctionZod>
export function list({
includeSystemSchemas = false,
includedSchemas,
excludedSchemas,
limit,
offset,
}: {
includeSystemSchemas?: boolean
includedSchemas?: string[]
excludedSchemas?: string[]
limit?: number
offset?: number
} = {}): {
sql: string
zod: typeof pgFunctionArrayZod
} {
let sql = /* SQL */ `
with f as (
${FUNCTIONS_SQL}
)
select
f.*
from f
`
const filter = filterByList(
includedSchemas,
excludedSchemas,
!includeSystemSchemas ? DEFAULT_SYSTEM_SCHEMAS : undefined
)
if (filter) {
sql += ` where schema ${filter}`
}
if (limit) {
sql = `${sql} limit ${limit}`
}
if (offset) {
sql = `${sql} offset ${offset}`
}
return {
sql,
zod: pgFunctionArrayZod,
}
}
export function retrieve({ id }: { id: number }): {
sql: string
zod: typeof pgFunctionOptionalZod
}
export function retrieve({
name,
schema,
args,
}: {
name: string
schema: string
args: string[]
}): {
sql: string
zod: typeof pgFunctionOptionalZod
}
export function retrieve({
id,
name,
schema = 'public',
args = [],
}: {
id?: number
name?: string
schema?: string
args?: string[]
}): {
sql: string
zod: typeof pgFunctionOptionalZod
} {
if (id) {
const sql = /* SQL */ `
with f as (
${FUNCTIONS_SQL}
)
select
f.*
from f where id = ${literal(id)};`
return {
sql,
zod: pgFunctionOptionalZod,
}
} else if (name && schema && args) {
const sql = /* SQL */ `with f as (
${FUNCTIONS_SQL}
)
select
f.*
from f join pg_proc as p on id = p.oid where schema = ${literal(
schema
)} and name = ${literal(name)} and p.proargtypes::text = ${
args.length
? /* SQL */ `(
select string_agg(type_oid::text, ' ') from (
select (
split_args.arr[
array_length(
split_args.arr,
1
)
]::regtype::oid
) as type_oid from (
select string_to_array(
unnest(
array[${args.map(literal)}]
),
' '
) as arr
) as split_args
) args
)`
: literal('')
}`
return {
sql,
zod: pgFunctionOptionalZod,
}
} else {
throw new Error('Must provide either id or name and schema')
}
}
export const pgFunctionCreateZod = z.object({
name: z.string(),
definition: z.string(),
args: z.array(z.string()).optional(),
behavior: z.enum(['IMMUTABLE', 'STABLE', 'VOLATILE']).optional(),
config_params: z.record(z.string(), z.string()).optional(),
schema: z.string().optional(),
language: z.string().optional(),
return_type: z.string().optional(),
security_definer: z.boolean().optional(),
})
export type PGFunctionCreate = z.infer<typeof pgFunctionCreateZod>
function _generateCreateFunctionSql(
{
name,
schema,
args,
definition,
return_type,
language,
behavior,
security_definer,
config_params,
}: PGFunctionCreate,
{ replace = false } = {}
): string {
return /* SQL */ `
CREATE ${replace ? 'OR REPLACE' : ''} FUNCTION ${ident(schema!)}.${ident(name!)}(${
args?.join(', ') || ''
})
RETURNS ${return_type}
AS ${literal(definition)}
LANGUAGE ${language}
${behavior}
CALLED ON NULL INPUT
${security_definer ? 'SECURITY DEFINER' : 'SECURITY INVOKER'}
${
config_params
? Object.entries(config_params)
.map(
([param, value]: string[]) =>
`SET ${param} ${value[0] === 'FROM CURRENT' ? 'FROM CURRENT' : 'TO ' + value}`
)
.join('\n')
: ''
};
`
}
export function create({
name,
schema = 'public',
args = [],
definition,
return_type = 'void',
language = 'sql',
behavior = 'VOLATILE',
security_definer = false,
config_params = {},
}: PGFunctionCreate) {
const sql = _generateCreateFunctionSql({
name,
schema,
args,
definition,
return_type,
language,
behavior,
security_definer,
config_params,
})
return {
sql,
zod: z.void(),
}
}
export const pgFunctionUpdateZod = z.object({
name: z.string().optional(),
schema: z.string().optional(),
definition: z.string().optional(),
})
export type PGFunctionUpdate = z.infer<typeof pgFunctionUpdateZod>
export function update(currentFunc: PGFunction, { name, schema, definition }: PGFunctionUpdate) {
const args = currentFunc.argument_types.split(', ')
const identityArgs = currentFunc.identity_argument_types
const updateDefinitionSql =
typeof definition === 'string'
? _generateCreateFunctionSql(
{
...currentFunc!,
definition,
args,
config_params: currentFunc!.config_params ?? {},
},
{ replace: true }
)
: ''
const updateNameSql =
name && name !== currentFunc!.name
? `ALTER FUNCTION ${ident(currentFunc!.schema)}.${ident(
currentFunc!.name
)}(${identityArgs}) RENAME TO ${ident(name)};`
: ''
const updateSchemaSql =
schema && schema !== currentFunc!.schema
? `ALTER FUNCTION ${ident(currentFunc!.schema)}.${ident(
name || currentFunc!.name
)}(${identityArgs}) SET SCHEMA ${ident(schema)};`
: ''
const sql = /* SQL */ `
DO LANGUAGE plpgsql $$
BEGIN
IF ${typeof definition === 'string' ? 'TRUE' : 'FALSE'} THEN
${updateDefinitionSql}
IF (
SELECT id
FROM (${FUNCTIONS_SQL}) AS f
WHERE f.schema = ${literal(currentFunc!.schema)}
AND f.name = ${literal(currentFunc!.name)}
AND f.identity_argument_types = ${literal(identityArgs)}
) != ${currentFunc.id} THEN
RAISE EXCEPTION 'Cannot find function "${currentFunc!.schema}"."${
currentFunc!.name
}"(${identityArgs})';
END IF;
END IF;
${updateNameSql}
${updateSchemaSql}
END;
$$;
`
return {
sql,
zod: z.void(),
}
}
export const pgFunctionDeleteZod = z.object({
cascade: z.boolean().default(false).optional(),
})
export type PGFunctionDelete = z.infer<typeof pgFunctionDeleteZod>
export function remove(func: PGFunction, { cascade = false }: PGFunctionDelete = {}) {
const sql = /* SQL */ `DROP FUNCTION ${ident(func.schema)}.${ident(func.name)}
(${func.identity_argument_types})
${cascade ? 'CASCADE' : 'RESTRICT'};`
return {
sql,
zod: z.void(),
}
}
+109
View File
@@ -0,0 +1,109 @@
export const FUNCTIONS_SQL = /* SQL */ `
-- CTE with sane arg_modes, arg_names, and arg_types.
-- All three are always of the same length.
-- All three include all args, including OUT and TABLE args.
with functions as (
select
*,
-- proargmodes is null when all arg modes are IN
coalesce(
p.proargmodes,
array_fill('i'::text, array[cardinality(coalesce(p.proallargtypes, p.proargtypes))])
) as arg_modes,
-- proargnames is null when all args are unnamed
coalesce(
p.proargnames,
array_fill(''::text, array[cardinality(coalesce(p.proallargtypes, p.proargtypes))])
) as arg_names,
-- proallargtypes is null when all arg modes are IN
coalesce(p.proallargtypes, p.proargtypes) as arg_types,
array_cat(
array_fill(false, array[pronargs - pronargdefaults]),
array_fill(true, array[pronargdefaults])) as arg_has_defaults
from
pg_proc as p
where
p.prokind = 'f'
)
select
f.oid::int8 as id,
n.nspname as schema,
f.proname as name,
l.lanname as language,
case
when l.lanname = 'internal' then ''
else f.prosrc
end as definition,
case
when l.lanname = 'internal' then f.prosrc
else pg_get_functiondef(f.oid)
end as complete_statement,
coalesce(f_args.args, '[]') as args,
pg_get_function_arguments(f.oid) as argument_types,
pg_get_function_identity_arguments(f.oid) as identity_argument_types,
f.prorettype::int8 as return_type_id,
pg_get_function_result(f.oid) as return_type,
nullif(rt.typrelid::int8, 0) as return_type_relation_id,
f.proretset as is_set_returning_function,
case
when f.provolatile = 'i' then 'IMMUTABLE'
when f.provolatile = 's' then 'STABLE'
when f.provolatile = 'v' then 'VOLATILE'
end as behavior,
f.prosecdef as security_definer,
f_config.config_params as config_params
from
functions f
left join pg_namespace n on f.pronamespace = n.oid
left join pg_language l on f.prolang = l.oid
left join pg_type rt on rt.oid = f.prorettype
left join (
select
oid,
jsonb_object_agg(param, value) filter (where param is not null) as config_params
from
(
select
oid,
(string_to_array(unnest(proconfig), '='))[1] as param,
(string_to_array(unnest(proconfig), '='))[2] as value
from
functions
) as t
group by
oid
) f_config on f_config.oid = f.oid
left join (
select
oid,
jsonb_agg(jsonb_build_object(
'mode', t2.mode,
'name', name,
'type_id', type_id,
'has_default', has_default
)) as args
from
(
select
oid,
unnest(arg_modes) as mode,
unnest(arg_names) as name,
unnest(arg_types)::int8 as type_id,
unnest(arg_has_defaults) as has_default
from
functions
) as t1,
lateral (
select
case
when t1.mode = 'i' then 'in'
when t1.mode = 'o' then 'out'
when t1.mode = 'b' then 'inout'
when t1.mode = 'v' then 'variadic'
else 'table'
end as mode
) as t2
group by
t1.oid
) f_args on f_args.oid = f.oid
`