Skip to content

Commit 003a26c

Browse files
authored
Merge pull request #137 from supabase/feat/func-def-update
feat: enable function definition update
2 parents bcbd59c + ce82698 commit 003a26c

File tree

4 files changed

+187
-65
lines changed

4 files changed

+187
-65
lines changed

src/lib/PostgresMetaFunctions.ts

+158-64
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ import { DEFAULT_SYSTEM_SCHEMAS } from './constants'
33
import { functionsSql } from './sql'
44
import { PostgresMetaResult, PostgresFunction } from './types'
55

6+
type FunctionInputs = {
7+
name: string
8+
definition: string
9+
args?: string[]
10+
behavior?: 'IMMUTABLE' | 'STABLE' | 'VOLATILE'
11+
config_params?: { [key: string]: string }
12+
schema?: string
13+
language?: string
14+
return_type?: string
15+
security_definer?: boolean
16+
}
17+
618
export default class PostgresMetaFunctions {
719
query: (sql: string) => Promise<PostgresMetaResult<any>>
820

@@ -53,31 +65,7 @@ export default class PostgresMetaFunctions {
5365
return { data: data[0], error }
5466
}
5567
} else if (name && schema && args) {
56-
const sql = `${enrichedFunctionsSql} JOIN pg_proc AS p ON id = p.oid WHERE schema = ${literal(
57-
schema
58-
)} AND name = ${literal(name)} AND p.proargtypes::text = ${
59-
args.length
60-
? `(
61-
SELECT STRING_AGG(type_oid::text, ' ') FROM (
62-
SELECT (
63-
split_args.arr[
64-
array_length(
65-
split_args.arr,
66-
1
67-
)
68-
]::regtype::oid
69-
) AS type_oid FROM (
70-
SELECT STRING_TO_ARRAY(
71-
UNNEST(
72-
ARRAY[${args.map(literal)}]
73-
),
74-
' '
75-
) AS arr
76-
) AS split_args
77-
) args
78-
);`
79-
: literal('')
80-
}`
68+
const sql = this.generateRetrieveFunctionSql({ name, schema, args })
8169
const { data, error } = await this.query(sql)
8270
if (error) {
8371
return { data, error }
@@ -106,32 +94,18 @@ export default class PostgresMetaFunctions {
10694
behavior = 'VOLATILE',
10795
security_definer = false,
10896
config_params = {},
109-
}: {
110-
name: string
111-
schema?: string
112-
args?: string[]
113-
definition: string
114-
return_type?: string
115-
language?: string
116-
behavior?: 'IMMUTABLE' | 'STABLE' | 'VOLATILE'
117-
security_definer?: boolean
118-
config_params?: { [key: string]: string }
119-
}): Promise<PostgresMetaResult<PostgresFunction>> {
120-
const sql = `
121-
CREATE FUNCTION ${ident(schema)}.${ident(name)}(${args.join(', ')})
122-
RETURNS ${return_type}
123-
AS ${literal(definition)}
124-
LANGUAGE ${language}
125-
${behavior}
126-
${security_definer ? 'SECURITY DEFINER' : 'SECURITY INVOKER'}
127-
${Object.entries(config_params)
128-
.map(
129-
([param, value]) =>
130-
`SET ${param} ${value[0] === 'FROM CURRENT' ? 'FROM CURRENT' : 'TO ' + value}`
131-
)
132-
.join('\n')}
133-
RETURNS NULL ON NULL INPUT;
134-
`
97+
}: FunctionInputs): Promise<PostgresMetaResult<PostgresFunction>> {
98+
const sql = this.generateCreateFunctionSql({
99+
name,
100+
schema,
101+
args,
102+
definition,
103+
return_type,
104+
language,
105+
behavior,
106+
security_definer,
107+
config_params,
108+
})
135109
const { error } = await this.query(sql)
136110
if (error) {
137111
return { data: null, error }
@@ -144,36 +118,78 @@ export default class PostgresMetaFunctions {
144118
{
145119
name,
146120
schema,
121+
definition,
147122
}: {
148123
name?: string
149124
schema?: string
125+
definition?: string
150126
}
151127
): Promise<PostgresMetaResult<PostgresFunction>> {
152-
const { data: old, error: retrieveError } = await this.retrieve({ id })
153-
if (retrieveError) {
154-
return { data: null, error: retrieveError }
128+
const { data: currentFunc, error } = await this.retrieve({ id })
129+
if (error) {
130+
return { data: null, error }
155131
}
156132

133+
const updateDefinitionSql = typeof definition === 'string' ? this.generateCreateFunctionSql(
134+
{ ...currentFunc!, definition },
135+
{ replace: true }
136+
) : ''
137+
138+
const retrieveFunctionSql = this.generateRetrieveFunctionSql(
139+
{
140+
schema: currentFunc!.schema,
141+
name: currentFunc!.name,
142+
args: currentFunc!.argument_types.split(', '),
143+
},
144+
{ terminateCommand: false }
145+
)
146+
157147
const updateNameSql =
158-
name && name !== old!.name
159-
? `ALTER FUNCTION ${ident(old!.schema)}.${ident(old!.name)}(${
160-
old!.argument_types
148+
name && name !== currentFunc!.name
149+
? `ALTER FUNCTION ${ident(currentFunc!.schema)}.${ident(currentFunc!.name)}(${
150+
currentFunc!.argument_types
161151
}) RENAME TO ${ident(name)};`
162152
: ''
163153

164154
const updateSchemaSql =
165-
schema && schema !== old!.schema
166-
? `ALTER FUNCTION ${ident(old!.schema)}.${ident(name || old!.name)}(${
167-
old!.argument_types
155+
schema && schema !== currentFunc!.schema
156+
? `ALTER FUNCTION ${ident(currentFunc!.schema)}.${ident(name || currentFunc!.name)}(${
157+
currentFunc!.argument_types
168158
}) SET SCHEMA ${ident(schema)};`
169159
: ''
170160

171-
const sql = `BEGIN; ${updateNameSql} ${updateSchemaSql} COMMIT;`
161+
const sql = `
162+
DO LANGUAGE plpgsql $$
163+
DECLARE
164+
function record;
165+
BEGIN
166+
IF ${typeof definition === 'string' ? 'TRUE' : 'FALSE'} THEN
167+
${updateDefinitionSql}
172168
173-
const { error } = await this.query(sql)
174-
if (error) {
175-
return { data: null, error }
169+
${retrieveFunctionSql} INTO function;
170+
171+
IF function.id != ${id} THEN
172+
RAISE EXCEPTION 'Cannot find function "${currentFunc!.schema}"."${currentFunc!.name}"(${
173+
currentFunc!.argument_types
174+
})';
175+
END IF;
176+
END IF;
177+
178+
${updateNameSql}
179+
180+
${updateSchemaSql}
181+
END;
182+
$$;
183+
`
184+
185+
{
186+
const { error } = await this.query(sql)
187+
188+
if (error) {
189+
return { data: null, error }
190+
}
176191
}
192+
177193
return await this.retrieve({ id })
178194
}
179195

@@ -196,6 +212,84 @@ export default class PostgresMetaFunctions {
196212
}
197213
return { data: func!, error: null }
198214
}
215+
216+
private generateCreateFunctionSql(
217+
{
218+
name,
219+
schema,
220+
args,
221+
argument_types,
222+
definition,
223+
return_type,
224+
language,
225+
behavior,
226+
security_definer,
227+
config_params,
228+
}: Partial<Omit<FunctionInputs, 'config_params'> & PostgresFunction>,
229+
{ replace = false, terminateCommand = true } = {}
230+
): string {
231+
return `
232+
CREATE ${replace ? 'OR REPLACE' : ''} FUNCTION ${ident(schema!)}.${ident(name!)}(${
233+
argument_types || args?.join(', ') || ''
234+
})
235+
RETURNS ${return_type}
236+
AS ${literal(definition)}
237+
LANGUAGE ${language}
238+
${behavior}
239+
CALLED ON NULL INPUT
240+
${security_definer ? 'SECURITY DEFINER' : 'SECURITY INVOKER'}
241+
${
242+
config_params
243+
? Object.entries(config_params)
244+
.map(
245+
([param, value]) =>
246+
`SET ${param} ${value[0] === 'FROM CURRENT' ? 'FROM CURRENT' : 'TO ' + value}`
247+
)
248+
.join('\n')
249+
: ''
250+
}
251+
${terminateCommand ? ';' : ''}
252+
`
253+
}
254+
255+
private generateRetrieveFunctionSql(
256+
{
257+
schema,
258+
name,
259+
args,
260+
}: {
261+
schema: string
262+
name: string
263+
args: string[]
264+
},
265+
{ terminateCommand = true } = {}
266+
): string {
267+
return `${enrichedFunctionsSql} JOIN pg_proc AS p ON id = p.oid WHERE schema = ${literal(
268+
schema
269+
)} AND name = ${literal(name)} AND p.proargtypes::text = ${
270+
args.length
271+
? `(
272+
SELECT STRING_AGG(type_oid::text, ' ') FROM (
273+
SELECT (
274+
split_args.arr[
275+
array_length(
276+
split_args.arr,
277+
1
278+
)
279+
]::regtype::oid
280+
) AS type_oid FROM (
281+
SELECT STRING_TO_ARRAY(
282+
UNNEST(
283+
ARRAY[${args.map(literal)}]
284+
),
285+
' '
286+
) AS arr
287+
) AS split_args
288+
) args
289+
) ${terminateCommand ? ';' : ''}`
290+
: literal('')
291+
}`
292+
}
199293
}
200294

201295
const enrichedFunctionsSql = `

src/lib/sql/functions.sql

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ SELECT
33
n.nspname AS schema,
44
p.proname AS name,
55
l.lanname AS language,
6+
CASE
7+
WHEN l.lanname = 'internal' THEN ''
8+
ELSE p.prosrc
9+
END AS definition,
610
CASE
711
WHEN l.lanname = 'internal' THEN p.prosrc
812
ELSE pg_get_functiondef(p.oid)
9-
END AS definition,
13+
END AS complete_statement,
1014
pg_get_function_arguments(p.oid) AS argument_types,
1115
t.typname AS return_type,
1216
CASE

src/lib/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const postgresFunctionSchema = Type.Object({
7676
name: Type.String(),
7777
language: Type.String(),
7878
definition: Type.String(),
79+
complete_statement: Type.String(),
7980
argument_types: Type.String(),
8081
return_type: Type.String(),
8182
behavior: Type.Union([

test/integration/index.spec.js

+23
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,17 @@ describe('/functions', () => {
215215
assert.strictEqual(newFunc.schema, 'public')
216216
assert.strictEqual(newFunc.argument_types, 'a smallint, b smallint')
217217
assert.strictEqual(newFunc.language, 'sql')
218+
assert.strictEqual(newFunc.definition, 'select a + b')
219+
assert.strictEqual(
220+
newFunc.complete_statement,
221+
'CREATE OR REPLACE FUNCTION public.test_func(a smallint, b smallint)\n' +
222+
' RETURNS integer\n' +
223+
' LANGUAGE sql\n' +
224+
' STABLE SECURITY DEFINER\n' +
225+
" SET search_path TO 'hooks', 'auth'\n" +
226+
" SET role TO 'postgres'\n" +
227+
'AS $function$select a + b$function$\n'
228+
)
218229
assert.strictEqual(newFunc.return_type, 'int4')
219230
assert.strictEqual(newFunc.behavior, 'STABLE')
220231
assert.strictEqual(newFunc.security_definer, true)
@@ -225,12 +236,24 @@ describe('/functions', () => {
225236
const updates = {
226237
name: 'test_func_renamed',
227238
schema: 'test_schema',
239+
definition: 'select b - a'
228240
}
229241

230242
let { data: updated } = await axios.patch(`${URL}/functions/${func.id}`, updates)
231243
assert.strictEqual(updated.id, func.id)
232244
assert.strictEqual(updated.name, 'test_func_renamed')
233245
assert.strictEqual(updated.schema, 'test_schema')
246+
assert.strictEqual(updated.definition, 'select b - a')
247+
assert.strictEqual(
248+
updated.complete_statement,
249+
'CREATE OR REPLACE FUNCTION test_schema.test_func_renamed(a smallint, b smallint)\n' +
250+
' RETURNS integer\n' +
251+
' LANGUAGE sql\n' +
252+
' STABLE SECURITY DEFINER\n' +
253+
" SET search_path TO 'hooks', 'auth'\n" +
254+
" SET role TO 'postgres'\n" +
255+
'AS $function$select b - a$function$\n'
256+
)
234257
})
235258
it('DELETE', async () => {
236259
await axios.delete(`${URL}/functions/${func.id}`)

0 commit comments

Comments
 (0)