diff --git a/apps/studio/components/interfaces/Auth/Hooks/FunctionSelector.tsx b/apps/studio/components/interfaces/Auth/Hooks/FunctionSelector.tsx index e2db6e5bea3..f0d598dda3e 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/FunctionSelector.tsx +++ b/apps/studio/components/interfaces/Auth/Hooks/FunctionSelector.tsx @@ -71,9 +71,11 @@ const FunctionSelector = ({ Failed to load functions + Error: {error.message} + diff --git a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx index 57ef8bbf90e..c8db848536d 100644 --- a/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx +++ b/apps/studio/components/interfaces/Database/Functions/CreateFunction/index.tsx @@ -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, diff --git a/apps/studio/components/interfaces/Database/Functions/DeleteFunction.tsx b/apps/studio/components/interfaces/Database/Functions/DeleteFunction.tsx index 0120b0caf11..057e373ec2f 100644 --- a/apps/studio/components/interfaces/Database/Functions/DeleteFunction.tsx +++ b/apps/studio/components/interfaces/Database/Functions/DeleteFunction.tsx @@ -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={ <> This will delete the function{' '} diff --git a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx index 8fe6fe053fd..a2bcebb700a 100644 --- a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx +++ b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx @@ -86,7 +86,7 @@ const FunctionsList = ({ }) if (isLoading) return - if (isError) + if (isError) return return ( <> diff --git a/apps/studio/components/ui/CodeEditor/Providers/PgSQLCompletionProvider.ts b/apps/studio/components/ui/CodeEditor/Providers/PgSQLCompletionProvider.ts index 599c67c4e04..f7bbbe4bce4 100644 --- a/apps/studio/components/ui/CodeEditor/Providers/PgSQLCompletionProvider.ts +++ b/apps/studio/components/ui/CodeEditor/Providers/PgSQLCompletionProvider.ts @@ -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) { items.push({ label: x.name, kind: monaco.languages.CompletionItemKind.Function, - detail: x.result_type, - documentation: x.description, + detail: x.return_type, insertText: x.name, }) }) diff --git a/apps/studio/components/ui/CodeEditor/Providers/PgSQLSignatureHelpProvider.ts b/apps/studio/components/ui/CodeEditor/Providers/PgSQLSignatureHelpProvider.ts index 829206810a6..03df0fc3d4c 100644 --- a/apps/studio/components/ui/CodeEditor/Providers/PgSQLSignatureHelpProvider.ts +++ b/apps/studio/components/ui/CodeEditor/Providers/PgSQLSignatureHelpProvider.ts @@ -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 } }), }) diff --git a/apps/studio/data/database-functions/database-functions-create-mutation.ts b/apps/studio/data/database-functions/database-functions-create-mutation.ts index 4481062a688..60553f8a86b 100644 --- a/apps/studio/data/database-functions/database-functions-create-mutation.ts +++ b/apps/studio/data/database-functions/database-functions-create-mutation.ts @@ -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 - } + payload: z.infer } 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 } type DatabaseFunctionCreateData = Awaited> @@ -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) { diff --git a/apps/studio/data/database-functions/database-functions-delete-mutation.ts b/apps/studio/data/database-functions/database-functions-delete-mutation.ts index 02714fa3f36..c37e9a973cb 100644 --- a/apps/studio/data/database-functions/database-functions-delete-mutation.ts +++ b/apps/studio/data/database-functions/database-functions-delete-mutation.ts @@ -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 } type DatabaseFunctionDeleteData = Awaited> @@ -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) { diff --git a/apps/studio/data/database-functions/database-functions-query.ts b/apps/studio/data/database-functions/database-functions-query.ts index c0729e02cb4..a6e7148ca79 100644 --- a/apps/studio/data/database-functions/database-functions-query.ts +++ b/apps/studio/data/database-functions/database-functions-query.ts @@ -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 -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> +export type DatabaseFunctionsData = z.infer export type DatabaseFunctionsError = ResponseError -export const useDatabaseFunctionsQuery = ( +export const useDatabaseFunctionsQuery = < + TData extends DatabaseFunctionsData = DatabaseFunctionsData, +>( { projectRef, connectionString }: DatabaseFunctionsVariables, - { - enabled = true, - ...options - }: UseQueryOptions = {} -) => - useQuery( - databaseFunctionsKeys.list(projectRef), - ({ signal }) => getDatabaseFunctions({ projectRef, connectionString }, signal), + options: UseQueryOptions = {} +) => { + return useExecuteSqlQuery( { - enabled: enabled && typeof projectRef !== 'undefined', + projectRef, + connectionString, + sql: pgMetaFunctionsList.sql, + queryKey: ['functions-list'], + }, + { + select(data) { + return (data as any)?.result ?? [] + }, ...options, } ) +} diff --git a/apps/studio/data/database-functions/database-functions-update-mutation.ts b/apps/studio/data/database-functions/database-functions-update-mutation.ts index 953f036c3e5..134d8ff7e81 100644 --- a/apps/studio/data/database-functions/database-functions-update-mutation.ts +++ b/apps/studio/data/database-functions/database-functions-update-mutation.ts @@ -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 } 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 } type DatabaseFunctionUpdateData = Awaited> @@ -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) { diff --git a/apps/studio/data/database-functions/keys.ts b/apps/studio/data/database-functions/keys.ts deleted file mode 100644 index 108e8567eee..00000000000 --- a/apps/studio/data/database-functions/keys.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const databaseFunctionsKeys = { - list: (projectRef: string | undefined) => ['projects', projectRef, 'database-functions'] as const, -} diff --git a/apps/studio/data/database-roles/database-roles-query.ts b/apps/studio/data/database-roles/database-roles-query.ts index e347af6f236..9a5569b07ae 100644 --- a/apps/studio/data/database-roles/database-roles-query.ts +++ b/apps/studio/data/database-roles/database-roles-query.ts @@ -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 const pgMetaRolesList = pgMeta.roles.list() export type DatabaseRolesData = z.infer -export type DatabaseRolesError = unknown +export type DatabaseRolesError = ExecuteSqlError export const useDatabaseRolesQuery = ( { projectRef, connectionString }: DatabaseRolesVariables, diff --git a/apps/studio/data/database/constraints-query.ts b/apps/studio/data/database/constraints-query.ts index 2c7db7134e7..ccf0ae456da 100644 --- a/apps/studio/data/database/constraints-query.ts +++ b/apps/studio/data/database/constraints-query.ts @@ -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 = ( { projectRef, connectionString, schema, table }: TableConstraintsVariables, diff --git a/apps/studio/data/database/database-size-query.ts b/apps/studio/data/database/database-size-query.ts index 3676d47365d..4a51c4b5e19 100644 --- a/apps/studio/data/database/database-size-query.ts +++ b/apps/studio/data/database/database-size-query.ts @@ -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 = ( { projectRef, connectionString }: DatabaseSizeVariables, diff --git a/apps/studio/data/database/entity-definitions-query.ts b/apps/studio/data/database/entity-definitions-query.ts index 9ee7702057f..92ceb7e5069 100644 --- a/apps/studio/data/database/entity-definitions-query.ts +++ b/apps/studio/data/database/entity-definitions-query.ts @@ -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, diff --git a/apps/studio/data/database/foreign-key-constraints-query.ts b/apps/studio/data/database/foreign-key-constraints-query.ts index 064bf0fa07b..3c348b11ec2 100644 --- a/apps/studio/data/database/foreign-key-constraints-query.ts +++ b/apps/studio/data/database/foreign-key-constraints-query.ts @@ -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, diff --git a/apps/studio/data/database/functions-query.ts b/apps/studio/data/database/functions-query.ts deleted file mode 100644 index e4607d7b878..00000000000 --- a/apps/studio/data/database/functions-query.ts +++ /dev/null @@ -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 = ( - { projectRef, connectionString }: FunctionsVariables, - options: UseQueryOptions = {} -) => { - 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, - } - ) -} diff --git a/apps/studio/data/database/indexes-query.ts b/apps/studio/data/database/indexes-query.ts index e45702479cc..c6f066caa36 100644 --- a/apps/studio/data/database/indexes-query.ts +++ b/apps/studio/data/database/indexes-query.ts @@ -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 = ( { schema, projectRef, connectionString }: IndexesVariables, diff --git a/apps/studio/data/database/keywords-query.ts b/apps/studio/data/database/keywords-query.ts index 3ada32f254c..7cfc9aa483f 100644 --- a/apps/studio/data/database/keywords-query.ts +++ b/apps/studio/data/database/keywords-query.ts @@ -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 = ( { projectRef, connectionString }: KeywordsVariables, diff --git a/apps/studio/data/database/max-connections-query.ts b/apps/studio/data/database/max-connections-query.ts index 876780b3df3..87a51ea04ac 100644 --- a/apps/studio/data/database/max-connections-query.ts +++ b/apps/studio/data/database/max-connections-query.ts @@ -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 = ( { projectRef, connectionString }: MaxConnectionsVariables, diff --git a/apps/studio/data/database/migrations-query.ts b/apps/studio/data/database/migrations-query.ts index 619cb0fdd41..aff79487eca 100644 --- a/apps/studio/data/database/migrations-query.ts +++ b/apps/studio/data/database/migrations-query.ts @@ -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 = ( { projectRef, connectionString }: MigrationsVariables, @@ -36,7 +36,7 @@ export const useMigrationsQuery = { + handleError: (error) => { if ( error.message.includes('relation "supabase_migrations.schema_migrations" does not exist') ) { diff --git a/apps/studio/data/database/schemas-query.ts b/apps/studio/data/database/schemas-query.ts index e93928dabc1..911f8fc88fa 100644 --- a/apps/studio/data/database/schemas-query.ts +++ b/apps/studio/data/database/schemas-query.ts @@ -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 const pgMetaSchemasList = pgMeta.schemas.list() export type SchemasData = z.infer -export type SchemasError = unknown +export type SchemasError = ExecuteSqlError export const useSchemasQuery = ( { projectRef, connectionString }: SchemasVariables, diff --git a/apps/studio/data/database/table-columns-query.ts b/apps/studio/data/database/table-columns-query.ts index d1cae3c3159..b680423d76c 100644 --- a/apps/studio/data/database/table-columns-query.ts +++ b/apps/studio/data/database/table-columns-query.ts @@ -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 = ( { projectRef, connectionString, table, schema }: TableColumnsVariables, diff --git a/apps/studio/data/database/table-definition-query.ts b/apps/studio/data/database/table-definition-query.ts index 3e3ed730b4e..9f29d08c6a7 100644 --- a/apps/studio/data/database/table-definition-query.ts +++ b/apps/studio/data/database/table-definition-query.ts @@ -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 = ( { projectRef, connectionString, schema, name }: TableDefinitionVariables, diff --git a/apps/studio/data/database/view-definition-query.ts b/apps/studio/data/database/view-definition-query.ts index 39e3b96317b..6fb09d538fc 100644 --- a/apps/studio/data/database/view-definition-query.ts +++ b/apps/studio/data/database/view-definition-query.ts @@ -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 = ( { projectRef, connectionString, schema, name }: ViewDefinitionVariables, diff --git a/apps/studio/data/entity-types/entity-type-query.ts b/apps/studio/data/entity-types/entity-type-query.ts index fd421fdd095..c650159ce52 100644 --- a/apps/studio/data/entity-types/entity-type-query.ts +++ b/apps/studio/data/entity-types/entity-type-query.ts @@ -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 = ( { projectRef, connectionString, id, ...args }: EntityTypeVariables, diff --git a/apps/studio/data/fdw/fdws-query.ts b/apps/studio/data/fdw/fdws-query.ts index 8851d54d6ee..2d40500870b 100644 --- a/apps/studio/data/fdw/fdws-query.ts +++ b/apps/studio/data/fdw/fdws-query.ts @@ -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 = ( { projectRef, connectionString }: FDWsVariables, diff --git a/apps/studio/data/read-replicas/replica-lag-query.ts b/apps/studio/data/read-replicas/replica-lag-query.ts index 423f7142754..9585e9eba42 100644 --- a/apps/studio/data/read-replicas/replica-lag-query.ts +++ b/apps/studio/data/read-replicas/replica-lag-query.ts @@ -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 = ( { projectRef, connectionString, id }: ReplicationLagVariables, diff --git a/apps/studio/data/sql/execute-sql-query.ts b/apps/studio/data/sql/execute-sql-query.ts index b3aec849476..0d5bdbb4322 100644 --- a/apps/studio/data/sql/execute-sql-query.ts +++ b/apps/studio/data/sql/execute-sql-query.ts @@ -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> -export type ExecuteSqlError = unknown +export type ExecuteSqlError = ResponseError export const useExecuteSqlQuery = ( { diff --git a/apps/studio/data/table-rows/table-rows-count-query.ts b/apps/studio/data/table-rows/table-rows-count-query.ts index 8f5c3e271e1..a95a7cd99b2 100644 --- a/apps/studio/data/table-rows/table-rows-count-query.ts +++ b/apps/studio/data/table-rows/table-rows-count-query.ts @@ -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 = ( { diff --git a/apps/studio/data/table-rows/table-rows-query.ts b/apps/studio/data/table-rows/table-rows-query.ts index cccb503f6d8..d04933191c9 100644 --- a/apps/studio/data/table-rows/table-rows-query.ts +++ b/apps/studio/data/table-rows/table-rows-query.ts @@ -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 = ( { projectRef, connectionString, queryKey, table, impersonatedRole, ...args }: TableRowsVariables, diff --git a/apps/studio/data/vault/vault-secrets-query.ts b/apps/studio/data/vault/vault-secrets-query.ts index 8ac2377ee97..ff3bf827be8 100644 --- a/apps/studio/data/vault/vault-secrets-query.ts +++ b/apps/studio/data/vault/vault-secrets-query.ts @@ -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 = ( { projectRef, connectionString }: VaultSecretsVariables, diff --git a/apps/studio/pages/project/[ref]/database/functions.tsx b/apps/studio/pages/project/[ref]/database/functions.tsx index 168eb7932e5..2883de5f38f 100644 --- a/apps/studio/pages/project/[ref]/database/functions.tsx +++ b/apps/studio/pages/project/[ref]/database/functions.tsx @@ -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() + const [selectedFunction, setSelectedFunction] = useState() const [showCreateFunctionForm, setShowCreateFunctionForm] = useState(false) const [showDeleteFunctionForm, setShowDeleteFunctionForm] = useState(false) diff --git a/apps/studio/pages/project/[ref]/sql/[id].tsx b/apps/studio/pages/project/[ref]/sql/[id].tsx index 87f616494ce..0c56b6b5f3a 100644 --- a/apps/studio/pages/project/[ref]/sql/[id].tsx +++ b/apps/studio/pages/project/[ref]/sql/[id].tsx @@ -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(() => { diff --git a/packages/pg-meta/src/helpers.ts b/packages/pg-meta/src/helpers.ts new file mode 100644 index 00000000000..4eadf84b438 --- /dev/null +++ b/packages/pg-meta/src/helpers.ts @@ -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 '' +} diff --git a/packages/pg-meta/src/index.ts b/packages/pg-meta/src/index.ts index aaadfe36832..60761df8f85 100644 --- a/packages/pg-meta/src/index.ts +++ b/packages/pg-meta/src/index.ts @@ -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, } diff --git a/packages/pg-meta/src/pg-meta-functions.ts b/packages/pg-meta/src/pg-meta-functions.ts new file mode 100644 index 00000000000..d2ffa10d6bb --- /dev/null +++ b/packages/pg-meta/src/pg-meta-functions.ts @@ -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 + +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 + +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 + +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 + +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(), + } +} diff --git a/packages/pg-meta/src/sql/functions.ts b/packages/pg-meta/src/sql/functions.ts new file mode 100644 index 00000000000..2afff047fc4 --- /dev/null +++ b/packages/pg-meta/src/sql/functions.ts @@ -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 +`