functions.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. import { afterAll, expect, test } from 'vitest'
  2. import pgMeta, { safeSql } from '../src/index'
  3. import type { PGFunction, PGSavedFunction } from '../src/pg-meta-functions'
  4. import { cleanupRoot, createTestDatabase } from './db/utils'
  5. // Test fixtures originate from `executeQuery` results that match the
  6. // API/database boundary; brand the raw-SQL fields so they satisfy
  7. // `update`/`remove` parameter types.
  8. const asSavedFunction = (fn: PGFunction): PGSavedFunction => fn as unknown as PGSavedFunction
  9. afterAll(async () => {
  10. await cleanupRoot()
  11. })
  12. const withTestDatabase = (
  13. name: string,
  14. fn: (db: Awaited<ReturnType<typeof createTestDatabase>>) => Promise<void>
  15. ) => {
  16. test(name, async () => {
  17. const db = await createTestDatabase()
  18. try {
  19. await fn(db)
  20. } finally {
  21. await db.cleanup()
  22. }
  23. })
  24. }
  25. withTestDatabase('list functions', async ({ executeQuery }) => {
  26. const { sql, zod } = pgMeta.functions.list()
  27. const res = zod.parse(await executeQuery(sql))
  28. // Test for the 'add' function created in init.sql
  29. const addFunction = res.find(({ name }) => name === 'add')
  30. expect(addFunction).toMatchInlineSnapshot(
  31. { id: expect.any(Number) },
  32. `
  33. {
  34. "args": [
  35. {
  36. "has_default": false,
  37. "mode": "in",
  38. "name": "",
  39. "type_id": 23,
  40. },
  41. {
  42. "has_default": false,
  43. "mode": "in",
  44. "name": "",
  45. "type_id": 23,
  46. },
  47. ],
  48. "argument_types": "integer, integer",
  49. "behavior": "IMMUTABLE",
  50. "complete_statement": "CREATE OR REPLACE FUNCTION public.add(integer, integer)
  51. RETURNS integer
  52. LANGUAGE sql
  53. IMMUTABLE STRICT
  54. AS $function$select $1 + $2;$function$
  55. ",
  56. "config_params": null,
  57. "definition": "select $1 + $2;",
  58. "id": Any<Number>,
  59. "identity_argument_types": "integer, integer",
  60. "is_set_returning_function": false,
  61. "language": "sql",
  62. "name": "add",
  63. "return_type": "integer",
  64. "return_type_id": 23,
  65. "return_type_relation_id": null,
  66. "schema": "public",
  67. "security_definer": false,
  68. }
  69. `
  70. )
  71. })
  72. withTestDatabase('list functions with included schemas', async ({ executeQuery }) => {
  73. const { sql, zod } = pgMeta.functions.list({
  74. includedSchemas: ['public'],
  75. })
  76. const res = zod.parse(await executeQuery(sql))
  77. expect(res.length).toBeGreaterThan(0)
  78. res.forEach((func) => {
  79. expect(func.schema).toBe('public')
  80. })
  81. })
  82. withTestDatabase('list functions with excluded schemas', async ({ executeQuery }) => {
  83. const { sql, zod } = pgMeta.functions.list({
  84. excludedSchemas: ['public'],
  85. })
  86. const res = zod.parse(await executeQuery(sql))
  87. res.forEach((func) => {
  88. expect(func.schema).not.toBe('public')
  89. })
  90. })
  91. withTestDatabase(
  92. 'list functions with excluded schemas and include System Schemas',
  93. async ({ executeQuery }) => {
  94. const { sql, zod } = pgMeta.functions.list({
  95. excludedSchemas: ['public'],
  96. includeSystemSchemas: true,
  97. })
  98. const res = zod.parse(await executeQuery(sql))
  99. expect(res.length).toBeGreaterThan(0)
  100. res.forEach((func) => {
  101. expect(func.schema).not.toBe('public')
  102. })
  103. }
  104. )
  105. withTestDatabase('retrieve, create, update, delete', async ({ executeQuery }) => {
  106. // Create function
  107. const { sql: createSql } = pgMeta.functions.create({
  108. name: 'test_func',
  109. schema: 'public',
  110. args: [safeSql`a int2`, safeSql`b int2`],
  111. definition: 'select a + b',
  112. return_type: safeSql`integer`,
  113. language: 'sql',
  114. behavior: 'STABLE',
  115. security_definer: true,
  116. config_params: { search_path: safeSql`hooks, auth`, role: safeSql`postgres` },
  117. })
  118. await executeQuery(createSql)
  119. // // Retrieve function
  120. const { sql: retrieveSql, zod: retrieveZod } = pgMeta.functions.retrieve({
  121. name: 'test_func',
  122. schema: 'public',
  123. args: ['a int2', 'b int2'],
  124. })
  125. const retrieve = await executeQuery(retrieveSql)
  126. const res = retrieveZod.parse(retrieve[0])
  127. const functionId = res!.id
  128. expect({ data: res, error: null }).toMatchInlineSnapshot(
  129. { data: { id: expect.any(Number) } },
  130. `
  131. {
  132. "data": {
  133. "args": [
  134. {
  135. "has_default": false,
  136. "mode": "in",
  137. "name": "a",
  138. "type_id": 21,
  139. },
  140. {
  141. "has_default": false,
  142. "mode": "in",
  143. "name": "b",
  144. "type_id": 21,
  145. },
  146. ],
  147. "argument_types": "a smallint, b smallint",
  148. "behavior": "STABLE",
  149. "complete_statement": "CREATE OR REPLACE FUNCTION public.test_func(a smallint, b smallint)
  150. RETURNS integer
  151. LANGUAGE sql
  152. STABLE SECURITY DEFINER
  153. SET search_path TO 'hooks', 'auth'
  154. SET role TO 'postgres'
  155. AS $function$select a + b$function$
  156. ",
  157. "config_params": {
  158. "role": "postgres",
  159. "search_path": "hooks, auth",
  160. },
  161. "definition": "select a + b",
  162. "id": Any<Number>,
  163. "identity_argument_types": "a smallint, b smallint",
  164. "is_set_returning_function": false,
  165. "language": "sql",
  166. "name": "test_func",
  167. "return_type": "integer",
  168. "return_type_id": 23,
  169. "return_type_relation_id": null,
  170. "schema": "public",
  171. "security_definer": true,
  172. },
  173. "error": null,
  174. }
  175. `
  176. )
  177. // create test_schema to move the function into:
  178. const { sql: createSchemaSql } = pgMeta.schemas.create({ name: 'test_schema' })
  179. await executeQuery(createSchemaSql)
  180. const { sql: updateSql } = pgMeta.functions.update(asSavedFunction(res!), {
  181. name: 'test_func_renamed',
  182. schema: 'test_schema',
  183. definition: 'select b - a',
  184. })
  185. await executeQuery(updateSql)
  186. const { sql: retrieveRenamedSql } = pgMeta.functions.retrieve({ id: functionId })
  187. const retrieveRenamed = await executeQuery(retrieveRenamedSql)
  188. const resUpdated = retrieveZod.parse(retrieveRenamed[0])
  189. expect({ data: resUpdated, error: null }).toMatchInlineSnapshot(
  190. { data: { id: expect.any(Number) } },
  191. `
  192. {
  193. "data": {
  194. "args": [
  195. {
  196. "has_default": false,
  197. "mode": "in",
  198. "name": "a",
  199. "type_id": 21,
  200. },
  201. {
  202. "has_default": false,
  203. "mode": "in",
  204. "name": "b",
  205. "type_id": 21,
  206. },
  207. ],
  208. "argument_types": "a smallint, b smallint",
  209. "behavior": "STABLE",
  210. "complete_statement": "CREATE OR REPLACE FUNCTION test_schema.test_func_renamed(a smallint, b smallint)
  211. RETURNS integer
  212. LANGUAGE sql
  213. STABLE SECURITY DEFINER
  214. SET role TO 'postgres'
  215. SET search_path TO 'hooks', 'auth'
  216. AS $function$select b - a$function$
  217. ",
  218. "config_params": {
  219. "role": "postgres",
  220. "search_path": "hooks, auth",
  221. },
  222. "definition": "select b - a",
  223. "id": Any<Number>,
  224. "identity_argument_types": "a smallint, b smallint",
  225. "is_set_returning_function": false,
  226. "language": "sql",
  227. "name": "test_func_renamed",
  228. "return_type": "integer",
  229. "return_type_id": 23,
  230. "return_type_relation_id": null,
  231. "schema": "test_schema",
  232. "security_definer": true,
  233. },
  234. "error": null,
  235. }
  236. `
  237. )
  238. // Remove function
  239. const { sql: removeSql } = pgMeta.functions.remove(asSavedFunction(resUpdated!))
  240. await executeQuery(removeSql)
  241. // Verify function is removed
  242. const { sql: verifyRemoveSql } = pgMeta.functions.retrieve({ id: functionId })
  243. const result = await executeQuery(verifyRemoveSql)
  244. expect(result).toHaveLength(0)
  245. })
  246. withTestDatabase('retrieve set-returning function', async ({ executeQuery }) => {
  247. // Retrieve function
  248. const { sql: retrieveSql, zod: retrieveZod } = pgMeta.functions.retrieve({
  249. schema: 'public',
  250. name: 'function_returning_set_of_rows',
  251. args: [],
  252. })
  253. const retrieve = await executeQuery(retrieveSql)
  254. const res = retrieveZod.parse(retrieve[0])
  255. expect(res).toMatchInlineSnapshot(
  256. {
  257. id: expect.any(Number),
  258. return_type_id: expect.any(Number),
  259. return_type_relation_id: expect.any(Number),
  260. },
  261. `
  262. {
  263. "args": [],
  264. "argument_types": "",
  265. "behavior": "STABLE",
  266. "complete_statement": "CREATE OR REPLACE FUNCTION public.function_returning_set_of_rows()
  267. RETURNS SETOF users
  268. LANGUAGE sql
  269. STABLE
  270. AS $function$
  271. select * from public.users;
  272. $function$
  273. ",
  274. "config_params": null,
  275. "definition": "
  276. select * from public.users;
  277. ",
  278. "id": Any<Number>,
  279. "identity_argument_types": "",
  280. "is_set_returning_function": true,
  281. "language": "sql",
  282. "name": "function_returning_set_of_rows",
  283. "return_type": "SETOF users",
  284. "return_type_id": Any<Number>,
  285. "return_type_relation_id": Any<Number>,
  286. "schema": "public",
  287. "security_definer": false,
  288. }
  289. `
  290. )
  291. })
  292. withTestDatabase('create function with various config_params values', async ({ executeQuery }) => {
  293. // Set initial application_name for consistent testing
  294. await executeQuery("SET application_name = 'current-app-name'")
  295. const { sql: createSql1 } = pgMeta.functions.create({
  296. name: 'test_func_config_1',
  297. schema: 'public',
  298. definition: 'select 1',
  299. return_type: safeSql`integer`,
  300. language: 'sql',
  301. config_params: {
  302. search_path: safeSql`''`, // Quoted empty string
  303. application_name: 'FROM CURRENT', // Special syntax: SET param FROM CURRENT
  304. work_mem: safeSql`'8MB'`, // Regular syntax: SET param TO value
  305. },
  306. })
  307. await executeQuery(createSql1)
  308. // Verify the function was created correctly
  309. const { sql: retrieveSql1, zod: retrieveZod } = pgMeta.functions.retrieve({
  310. name: 'test_func_config_1',
  311. schema: 'public',
  312. args: [],
  313. })
  314. const result1 = retrieveZod.parse((await executeQuery(retrieveSql1))[0])
  315. expect(result1).toBeDefined()
  316. expect(result1!.config_params).toEqual({
  317. search_path: '""',
  318. application_name: 'current-app-name',
  319. work_mem: '8MB',
  320. })
  321. // Clean up
  322. const { sql: removeSql1 } = pgMeta.functions.remove(asSavedFunction(result1!))
  323. await executeQuery(removeSql1)
  324. })
  325. withTestDatabase(
  326. 'create function with namespaced custom GUC config_params',
  327. async ({ executeQuery }) => {
  328. const { sql: createSql } = pgMeta.functions.create({
  329. name: 'test_func_namespaced_guc',
  330. schema: 'public',
  331. definition: 'select 1',
  332. return_type: safeSql`integer`,
  333. language: 'sql',
  334. config_params: {
  335. 'app.jwt_secret': safeSql`'top-secret'`,
  336. },
  337. })
  338. await executeQuery(createSql)
  339. const { sql: retrieveSql, zod: retrieveZod } = pgMeta.functions.retrieve({
  340. name: 'test_func_namespaced_guc',
  341. schema: 'public',
  342. args: [],
  343. })
  344. const result = retrieveZod.parse((await executeQuery(retrieveSql))[0])
  345. expect(result).toBeDefined()
  346. expect(result!.config_params).toEqual({
  347. 'app.jwt_secret': 'top-secret',
  348. })
  349. const { sql: removeSql } = pgMeta.functions.remove(asSavedFunction(result!))
  350. await executeQuery(removeSql)
  351. }
  352. )