mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## TL;DR Restores handling for functions with `search_path` set to `''` editing them in the UI was failing with a Postgres `zero-length delimited identifier` error since the SafeSql refactor dropped the empty-string sentinel conversion ## ref - closes #48149 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Preserved empty `search_path` configuration values when updating database functions. * Prevented empty configuration values from being altered or lost during function updates. * **Tests** * Added coverage verifying that function definitions can be updated without changing an existing empty `search_path` setting. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
415 lines
12 KiB
TypeScript
415 lines
12 KiB
TypeScript
import { afterAll, expect, test } from 'vitest'
|
|
|
|
import pgMeta, { safeSql } from '../src/index'
|
|
import type { PGFunction, PGSavedFunction } from '../src/pg-meta-functions'
|
|
import { cleanupRoot, createTestDatabase } from './db/utils'
|
|
|
|
// Test fixtures originate from `executeQuery` results that match the
|
|
// API/database boundary; brand the raw-SQL fields so they satisfy
|
|
// `update`/`remove` parameter types.
|
|
const asSavedFunction = (fn: PGFunction): PGSavedFunction => fn as unknown as PGSavedFunction
|
|
|
|
afterAll(async () => {
|
|
await cleanupRoot()
|
|
})
|
|
|
|
const withTestDatabase = (
|
|
name: string,
|
|
fn: (db: Awaited<ReturnType<typeof createTestDatabase>>) => Promise<void>
|
|
) => {
|
|
test(name, async () => {
|
|
const db = await createTestDatabase()
|
|
try {
|
|
await fn(db)
|
|
} finally {
|
|
await db.cleanup()
|
|
}
|
|
})
|
|
}
|
|
|
|
withTestDatabase('list functions', async ({ executeQuery }) => {
|
|
const { sql, zod } = pgMeta.functions.list()
|
|
const res = zod.parse(await executeQuery(sql))
|
|
|
|
// Test for the 'add' function created in init.sql
|
|
const addFunction = res.find(({ name }) => name === 'add')
|
|
expect(addFunction).toMatchInlineSnapshot(
|
|
{ id: expect.any(Number) },
|
|
`
|
|
{
|
|
"args": [
|
|
{
|
|
"has_default": false,
|
|
"mode": "in",
|
|
"name": "",
|
|
"type_id": 23,
|
|
},
|
|
{
|
|
"has_default": false,
|
|
"mode": "in",
|
|
"name": "",
|
|
"type_id": 23,
|
|
},
|
|
],
|
|
"argument_types": "integer, integer",
|
|
"behavior": "IMMUTABLE",
|
|
"complete_statement": "CREATE OR REPLACE FUNCTION public.add(integer, integer)
|
|
RETURNS integer
|
|
LANGUAGE sql
|
|
IMMUTABLE STRICT
|
|
AS $function$select $1 + $2;$function$
|
|
",
|
|
"config_params": null,
|
|
"definition": "select $1 + $2;",
|
|
"id": Any<Number>,
|
|
"identity_argument_types": "integer, integer",
|
|
"is_set_returning_function": false,
|
|
"language": "sql",
|
|
"name": "add",
|
|
"return_type": "integer",
|
|
"return_type_id": 23,
|
|
"return_type_relation_id": null,
|
|
"schema": "public",
|
|
"security_definer": false,
|
|
"type": "function",
|
|
}
|
|
`
|
|
)
|
|
})
|
|
|
|
withTestDatabase('list functions with included schemas', async ({ executeQuery }) => {
|
|
const { sql, zod } = pgMeta.functions.list({
|
|
includedSchemas: ['public'],
|
|
})
|
|
const res = zod.parse(await executeQuery(sql))
|
|
|
|
expect(res.length).toBeGreaterThan(0)
|
|
res.forEach((func) => {
|
|
expect(func.schema).toBe('public')
|
|
})
|
|
})
|
|
|
|
withTestDatabase('list functions with excluded schemas', async ({ executeQuery }) => {
|
|
const { sql, zod } = pgMeta.functions.list({
|
|
excludedSchemas: ['public'],
|
|
})
|
|
const res = zod.parse(await executeQuery(sql))
|
|
|
|
res.forEach((func) => {
|
|
expect(func.schema).not.toBe('public')
|
|
})
|
|
})
|
|
|
|
withTestDatabase(
|
|
'list functions with excluded schemas and include System Schemas',
|
|
async ({ executeQuery }) => {
|
|
const { sql, zod } = pgMeta.functions.list({
|
|
excludedSchemas: ['public'],
|
|
includeSystemSchemas: true,
|
|
})
|
|
const res = zod.parse(await executeQuery(sql))
|
|
|
|
expect(res.length).toBeGreaterThan(0)
|
|
res.forEach((func) => {
|
|
expect(func.schema).not.toBe('public')
|
|
})
|
|
}
|
|
)
|
|
|
|
withTestDatabase('retrieve, create, update, delete', async ({ executeQuery }) => {
|
|
// Create function
|
|
const { sql: createSql } = pgMeta.functions.create({
|
|
name: 'test_func',
|
|
schema: 'public',
|
|
args: [safeSql`a int2`, safeSql`b int2`],
|
|
definition: 'select a + b',
|
|
return_type: safeSql`integer`,
|
|
language: 'sql',
|
|
behavior: 'STABLE',
|
|
security_definer: true,
|
|
config_params: { search_path: safeSql`hooks, auth`, role: safeSql`postgres` },
|
|
})
|
|
await executeQuery(createSql)
|
|
|
|
// // Retrieve function
|
|
const { sql: retrieveSql, zod: retrieveZod } = pgMeta.functions.retrieve({
|
|
name: 'test_func',
|
|
schema: 'public',
|
|
args: ['a int2', 'b int2'],
|
|
})
|
|
const retrieve = await executeQuery(retrieveSql)
|
|
const res = retrieveZod.parse(retrieve[0])
|
|
const functionId = res!.id
|
|
expect({ data: res, error: null }).toMatchInlineSnapshot(
|
|
{ data: { id: expect.any(Number) } },
|
|
`
|
|
{
|
|
"data": {
|
|
"args": [
|
|
{
|
|
"has_default": false,
|
|
"mode": "in",
|
|
"name": "a",
|
|
"type_id": 21,
|
|
},
|
|
{
|
|
"has_default": false,
|
|
"mode": "in",
|
|
"name": "b",
|
|
"type_id": 21,
|
|
},
|
|
],
|
|
"argument_types": "a smallint, b smallint",
|
|
"behavior": "STABLE",
|
|
"complete_statement": "CREATE OR REPLACE FUNCTION public.test_func(a smallint, b smallint)
|
|
RETURNS integer
|
|
LANGUAGE sql
|
|
STABLE SECURITY DEFINER
|
|
SET search_path TO 'hooks', 'auth'
|
|
SET role TO 'postgres'
|
|
AS $function$select a + b$function$
|
|
",
|
|
"config_params": {
|
|
"role": "postgres",
|
|
"search_path": "hooks, auth",
|
|
},
|
|
"definition": "select a + b",
|
|
"id": Any<Number>,
|
|
"identity_argument_types": "a smallint, b smallint",
|
|
"is_set_returning_function": false,
|
|
"language": "sql",
|
|
"name": "test_func",
|
|
"return_type": "integer",
|
|
"return_type_id": 23,
|
|
"return_type_relation_id": null,
|
|
"schema": "public",
|
|
"security_definer": true,
|
|
"type": "function",
|
|
},
|
|
"error": null,
|
|
}
|
|
`
|
|
)
|
|
// create test_schema to move the function into:
|
|
const { sql: createSchemaSql } = pgMeta.schemas.create({ name: 'test_schema' })
|
|
await executeQuery(createSchemaSql)
|
|
const { sql: updateSql } = pgMeta.functions.update(asSavedFunction(res!), {
|
|
name: 'test_func_renamed',
|
|
schema: 'test_schema',
|
|
definition: 'select b - a',
|
|
})
|
|
await executeQuery(updateSql)
|
|
|
|
const { sql: retrieveRenamedSql } = pgMeta.functions.retrieve({ id: functionId })
|
|
const retrieveRenamed = await executeQuery(retrieveRenamedSql)
|
|
const resUpdated = retrieveZod.parse(retrieveRenamed[0])
|
|
expect({ data: resUpdated, error: null }).toMatchInlineSnapshot(
|
|
{ data: { id: expect.any(Number) } },
|
|
`
|
|
{
|
|
"data": {
|
|
"args": [
|
|
{
|
|
"has_default": false,
|
|
"mode": "in",
|
|
"name": "a",
|
|
"type_id": 21,
|
|
},
|
|
{
|
|
"has_default": false,
|
|
"mode": "in",
|
|
"name": "b",
|
|
"type_id": 21,
|
|
},
|
|
],
|
|
"argument_types": "a smallint, b smallint",
|
|
"behavior": "STABLE",
|
|
"complete_statement": "CREATE OR REPLACE FUNCTION test_schema.test_func_renamed(a smallint, b smallint)
|
|
RETURNS integer
|
|
LANGUAGE sql
|
|
STABLE SECURITY DEFINER
|
|
SET role TO 'postgres'
|
|
SET search_path TO 'hooks', 'auth'
|
|
AS $function$select b - a$function$
|
|
",
|
|
"config_params": {
|
|
"role": "postgres",
|
|
"search_path": "hooks, auth",
|
|
},
|
|
"definition": "select b - a",
|
|
"id": Any<Number>,
|
|
"identity_argument_types": "a smallint, b smallint",
|
|
"is_set_returning_function": false,
|
|
"language": "sql",
|
|
"name": "test_func_renamed",
|
|
"return_type": "integer",
|
|
"return_type_id": 23,
|
|
"return_type_relation_id": null,
|
|
"schema": "test_schema",
|
|
"security_definer": true,
|
|
"type": "function",
|
|
},
|
|
"error": null,
|
|
}
|
|
`
|
|
)
|
|
|
|
// Remove function
|
|
const { sql: removeSql } = pgMeta.functions.remove(asSavedFunction(resUpdated!))
|
|
await executeQuery(removeSql)
|
|
// Verify function is removed
|
|
const { sql: verifyRemoveSql } = pgMeta.functions.retrieve({ id: functionId })
|
|
const result = await executeQuery(verifyRemoveSql)
|
|
expect(result).toHaveLength(0)
|
|
})
|
|
|
|
withTestDatabase('retrieve set-returning function', async ({ executeQuery }) => {
|
|
// Retrieve function
|
|
const { sql: retrieveSql, zod: retrieveZod } = pgMeta.functions.retrieve({
|
|
schema: 'public',
|
|
name: 'function_returning_set_of_rows',
|
|
args: [],
|
|
})
|
|
const retrieve = await executeQuery(retrieveSql)
|
|
const res = retrieveZod.parse(retrieve[0])
|
|
expect(res).toMatchInlineSnapshot(
|
|
{
|
|
id: expect.any(Number),
|
|
return_type_id: expect.any(Number),
|
|
return_type_relation_id: expect.any(Number),
|
|
},
|
|
`
|
|
{
|
|
"args": [],
|
|
"argument_types": "",
|
|
"behavior": "STABLE",
|
|
"complete_statement": "CREATE OR REPLACE FUNCTION public.function_returning_set_of_rows()
|
|
RETURNS SETOF users
|
|
LANGUAGE sql
|
|
STABLE
|
|
AS $function$
|
|
select * from public.users;
|
|
$function$
|
|
",
|
|
"config_params": null,
|
|
"definition": "
|
|
select * from public.users;
|
|
",
|
|
"id": Any<Number>,
|
|
"identity_argument_types": "",
|
|
"is_set_returning_function": true,
|
|
"language": "sql",
|
|
"name": "function_returning_set_of_rows",
|
|
"return_type": "SETOF users",
|
|
"return_type_id": Any<Number>,
|
|
"return_type_relation_id": Any<Number>,
|
|
"schema": "public",
|
|
"security_definer": false,
|
|
"type": "function",
|
|
}
|
|
`
|
|
)
|
|
})
|
|
|
|
withTestDatabase('create function with various config_params values', async ({ executeQuery }) => {
|
|
// Set initial application_name for consistent testing
|
|
await executeQuery("SET application_name = 'current-app-name'")
|
|
|
|
const { sql: createSql1 } = pgMeta.functions.create({
|
|
name: 'test_func_config_1',
|
|
schema: 'public',
|
|
definition: 'select 1',
|
|
return_type: safeSql`integer`,
|
|
language: 'sql',
|
|
config_params: {
|
|
search_path: safeSql`''`, // Quoted empty string
|
|
application_name: 'FROM CURRENT', // Special syntax: SET param FROM CURRENT
|
|
work_mem: safeSql`'8MB'`, // Regular syntax: SET param TO value
|
|
},
|
|
})
|
|
await executeQuery(createSql1)
|
|
|
|
// Verify the function was created correctly
|
|
const { sql: retrieveSql1, zod: retrieveZod } = pgMeta.functions.retrieve({
|
|
name: 'test_func_config_1',
|
|
schema: 'public',
|
|
args: [],
|
|
})
|
|
const result1 = retrieveZod.parse((await executeQuery(retrieveSql1))[0])
|
|
|
|
expect(result1).toBeDefined()
|
|
expect(result1!.config_params).toEqual({
|
|
search_path: '""',
|
|
application_name: 'current-app-name',
|
|
work_mem: '8MB',
|
|
})
|
|
|
|
// Clean up
|
|
const { sql: removeSql1 } = pgMeta.functions.remove(asSavedFunction(result1!))
|
|
await executeQuery(removeSql1)
|
|
})
|
|
|
|
withTestDatabase('update function with empty string search_path', async ({ executeQuery }) => {
|
|
const { sql: createSql } = pgMeta.functions.create({
|
|
name: 'test_func_empty_search_path',
|
|
schema: 'public',
|
|
definition: 'select 1',
|
|
return_type: safeSql`integer`,
|
|
language: 'sql',
|
|
config_params: { search_path: safeSql`''` },
|
|
})
|
|
await executeQuery(createSql)
|
|
|
|
const { sql: retrieveSql, zod: retrieveZod } = pgMeta.functions.retrieve({
|
|
name: 'test_func_empty_search_path',
|
|
schema: 'public',
|
|
args: [],
|
|
})
|
|
const result = retrieveZod.parse((await executeQuery(retrieveSql))[0])
|
|
expect(result!.config_params).toEqual({ search_path: '""' })
|
|
|
|
const { sql: updateSql } = pgMeta.functions.update(asSavedFunction(result!), {
|
|
definition: 'select 2',
|
|
})
|
|
await executeQuery(updateSql)
|
|
|
|
const resultUpdated = retrieveZod.parse((await executeQuery(retrieveSql))[0])
|
|
expect(resultUpdated!.definition).toBe('select 2')
|
|
expect(resultUpdated!.config_params).toEqual({ search_path: '""' })
|
|
|
|
const { sql: removeSql } = pgMeta.functions.remove(asSavedFunction(resultUpdated!))
|
|
await executeQuery(removeSql)
|
|
})
|
|
|
|
withTestDatabase(
|
|
'create function with namespaced custom GUC config_params',
|
|
async ({ executeQuery }) => {
|
|
const { sql: createSql } = pgMeta.functions.create({
|
|
name: 'test_func_namespaced_guc',
|
|
schema: 'public',
|
|
definition: 'select 1',
|
|
return_type: safeSql`integer`,
|
|
language: 'sql',
|
|
config_params: {
|
|
'app.jwt_secret': safeSql`'top-secret'`,
|
|
},
|
|
})
|
|
await executeQuery(createSql)
|
|
|
|
const { sql: retrieveSql, zod: retrieveZod } = pgMeta.functions.retrieve({
|
|
name: 'test_func_namespaced_guc',
|
|
schema: 'public',
|
|
args: [],
|
|
})
|
|
const result = retrieveZod.parse((await executeQuery(retrieveSql))[0])
|
|
|
|
expect(result).toBeDefined()
|
|
expect(result!.config_params).toEqual({
|
|
'app.jwt_secret': 'top-secret',
|
|
})
|
|
|
|
const { sql: removeSql } = pgMeta.functions.remove(asSavedFunction(result!))
|
|
await executeQuery(removeSql)
|
|
}
|
|
)
|