tables.test.ts 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493
  1. import { afterAll, beforeAll, expect, test } from 'vitest'
  2. import pgMeta from '../src/index'
  3. import { cleanupRoot, createTestDatabase } from './db/utils'
  4. beforeAll(async () => {
  5. // Any global setup if needed
  6. })
  7. afterAll(async () => {
  8. await cleanupRoot()
  9. })
  10. const cleanNondet = (x: any) => {
  11. const { columns, primary_keys, relationships, ...rest2 } = x
  12. return {
  13. columns: columns.map(({ id, table_id, ...rest }: any) => rest),
  14. primary_keys: primary_keys.map(({ table_id, ...rest }: any) => rest),
  15. relationships: relationships.map(({ id, ...rest }: any) => rest),
  16. ...rest2,
  17. }
  18. }
  19. type TestDb = Awaited<ReturnType<typeof createTestDatabase>>
  20. const withTestDatabase = (name: string, fn: (db: TestDb) => Promise<void>) => {
  21. test(name, async () => {
  22. const db = await createTestDatabase()
  23. try {
  24. await fn(db)
  25. } finally {
  26. await db.cleanup()
  27. }
  28. })
  29. }
  30. /** Original tests ported from postgres-meta */
  31. withTestDatabase('list tables', async ({ executeQuery }) => {
  32. const { sql, zod } = await pgMeta.tables.list()
  33. const res = zod.parse(await executeQuery(sql))
  34. const { columns, primary_keys, relationships, ...rest } = res.find(
  35. ({ name }) => name === 'users'
  36. )!
  37. expect({
  38. columns: columns!.map(({ id, table_id, ...rest }) => rest),
  39. primary_keys: primary_keys.map(({ table_id, ...rest }) => rest),
  40. relationships: relationships.map(({ id, ...rest }) => rest),
  41. ...rest,
  42. }).toMatchInlineSnapshot(
  43. {
  44. bytes: expect.any(Number),
  45. dead_rows_estimate: expect.any(Number),
  46. id: expect.any(Number),
  47. live_rows_estimate: expect.any(Number),
  48. size: expect.any(String),
  49. },
  50. `
  51. {
  52. "bytes": Any<Number>,
  53. "columns": [
  54. {
  55. "check": null,
  56. "comment": null,
  57. "data_type": "bigint",
  58. "default_value": null,
  59. "enums": [],
  60. "format": "int8",
  61. "format_schema": "pg_catalog",
  62. "identity_generation": "BY DEFAULT",
  63. "is_generated": false,
  64. "is_identity": true,
  65. "is_nullable": false,
  66. "is_unique": false,
  67. "is_updatable": true,
  68. "name": "id",
  69. "ordinal_position": 1,
  70. "schema": "public",
  71. "table": "users",
  72. },
  73. {
  74. "check": null,
  75. "comment": null,
  76. "data_type": "text",
  77. "default_value": null,
  78. "enums": [],
  79. "format": "text",
  80. "format_schema": "pg_catalog",
  81. "identity_generation": null,
  82. "is_generated": false,
  83. "is_identity": false,
  84. "is_nullable": true,
  85. "is_unique": false,
  86. "is_updatable": true,
  87. "name": "name",
  88. "ordinal_position": 2,
  89. "schema": "public",
  90. "table": "users",
  91. },
  92. {
  93. "check": null,
  94. "comment": null,
  95. "data_type": "USER-DEFINED",
  96. "default_value": "'ACTIVE'::user_status",
  97. "enums": [
  98. "ACTIVE",
  99. "INACTIVE",
  100. ],
  101. "format": "user_status",
  102. "format_schema": "public",
  103. "identity_generation": null,
  104. "is_generated": false,
  105. "is_identity": false,
  106. "is_nullable": true,
  107. "is_unique": false,
  108. "is_updatable": true,
  109. "name": "status",
  110. "ordinal_position": 3,
  111. "schema": "public",
  112. "table": "users",
  113. },
  114. ],
  115. "comment": null,
  116. "dead_rows_estimate": Any<Number>,
  117. "id": Any<Number>,
  118. "live_rows_estimate": Any<Number>,
  119. "name": "users",
  120. "primary_keys": [
  121. {
  122. "name": "id",
  123. "schema": "public",
  124. "table_name": "users",
  125. },
  126. ],
  127. "relationships": [
  128. {
  129. "constraint_name": "todos_user-id_fkey",
  130. "source_column_name": "user-id",
  131. "source_schema": "public",
  132. "source_table_name": "todos",
  133. "target_column_name": "id",
  134. "target_table_name": "users",
  135. "target_table_schema": "public",
  136. },
  137. {
  138. "constraint_name": "user_details_user_id_fkey",
  139. "source_column_name": "user_id",
  140. "source_schema": "public",
  141. "source_table_name": "user_details",
  142. "target_column_name": "id",
  143. "target_table_name": "users",
  144. "target_table_schema": "public",
  145. },
  146. ],
  147. "replica_identity": "DEFAULT",
  148. "rls_enabled": false,
  149. "rls_forced": false,
  150. "schema": "public",
  151. "size": Any<String>,
  152. }
  153. `
  154. )
  155. })
  156. withTestDatabase('list tables without columns', async ({ executeQuery }) => {
  157. const { sql, zod } = await pgMeta.tables.list({ includeColumns: false })
  158. const res = zod.parse(await executeQuery(sql))
  159. //@ts-expect-error columns doesn't exist at type level if includeColumns is false
  160. const { columns, primary_keys, relationships, ...rest } = res.find(
  161. ({ name }) => name === 'users'
  162. )!
  163. expect({
  164. primary_keys: primary_keys.map(({ table_id, ...rest }) => rest),
  165. relationships: relationships.map(({ id, ...rest }) => rest),
  166. ...rest,
  167. }).toMatchInlineSnapshot(
  168. {
  169. bytes: expect.any(Number),
  170. dead_rows_estimate: expect.any(Number),
  171. id: expect.any(Number),
  172. live_rows_estimate: expect.any(Number),
  173. size: expect.any(String),
  174. },
  175. `
  176. {
  177. "bytes": Any<Number>,
  178. "comment": null,
  179. "dead_rows_estimate": Any<Number>,
  180. "id": Any<Number>,
  181. "live_rows_estimate": Any<Number>,
  182. "name": "users",
  183. "primary_keys": [
  184. {
  185. "name": "id",
  186. "schema": "public",
  187. "table_name": "users",
  188. },
  189. ],
  190. "relationships": [
  191. {
  192. "constraint_name": "todos_user-id_fkey",
  193. "source_column_name": "user-id",
  194. "source_schema": "public",
  195. "source_table_name": "todos",
  196. "target_column_name": "id",
  197. "target_table_name": "users",
  198. "target_table_schema": "public",
  199. },
  200. {
  201. "constraint_name": "user_details_user_id_fkey",
  202. "source_column_name": "user_id",
  203. "source_schema": "public",
  204. "source_table_name": "user_details",
  205. "target_column_name": "id",
  206. "target_table_name": "users",
  207. "target_table_schema": "public",
  208. },
  209. ],
  210. "replica_identity": "DEFAULT",
  211. "rls_enabled": false,
  212. "rls_forced": false,
  213. "schema": "public",
  214. "size": Any<String>,
  215. }
  216. `
  217. )
  218. })
  219. withTestDatabase('list tables with included schemas', async ({ executeQuery }) => {
  220. const { sql, zod } = await pgMeta.tables.list({
  221. includedSchemas: ['public'],
  222. })
  223. const res = zod.parse(await executeQuery(sql))
  224. expect(res.length).toBeGreaterThan(0)
  225. res.forEach((table) => {
  226. expect(table.schema).toBe('public')
  227. })
  228. })
  229. withTestDatabase('list tables with excluded schemas', async ({ executeQuery }) => {
  230. const { sql, zod } = await pgMeta.tables.list({
  231. excludedSchemas: ['public'],
  232. })
  233. const res = zod.parse(await executeQuery(sql))
  234. res.forEach((table) => {
  235. expect(table.schema).not.toBe('public')
  236. })
  237. })
  238. withTestDatabase(
  239. 'list tables with excluded schemas and include System Schemas',
  240. async ({ executeQuery }) => {
  241. const { sql, zod } = await pgMeta.tables.list({
  242. excludedSchemas: ['public'],
  243. includeSystemSchemas: true,
  244. })
  245. const res = zod.parse(await executeQuery(sql))
  246. expect(res.length).toBeGreaterThan(0)
  247. res.forEach((table) => {
  248. expect(table.schema).not.toBe('public')
  249. })
  250. }
  251. )
  252. withTestDatabase('create, retrieve, update, and delete table', async ({ executeQuery }) => {
  253. // Create table
  254. const { sql: createSql } = await pgMeta.tables.create({
  255. name: 'test',
  256. comment: 'foo',
  257. })
  258. await executeQuery(createSql)
  259. // Retrieve the created table
  260. const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.tables.retrieve({
  261. name: 'test',
  262. schema: 'public',
  263. })
  264. const retrieveRes = retrieveZod.parse((await executeQuery(retrieveSql))[0])
  265. expect(retrieveRes).toMatchInlineSnapshot(
  266. {
  267. bytes: expect.any(Number),
  268. dead_rows_estimate: expect.any(Number),
  269. id: expect.any(Number),
  270. live_rows_estimate: expect.any(Number),
  271. size: expect.any(String),
  272. },
  273. `
  274. {
  275. "bytes": Any<Number>,
  276. "columns": [],
  277. "comment": "foo",
  278. "dead_rows_estimate": Any<Number>,
  279. "id": Any<Number>,
  280. "live_rows_estimate": Any<Number>,
  281. "name": "test",
  282. "primary_keys": [],
  283. "relationships": [],
  284. "replica_identity": "DEFAULT",
  285. "rls_enabled": false,
  286. "rls_forced": false,
  287. "schema": "public",
  288. "size": Any<String>,
  289. }
  290. `
  291. )
  292. // Update table
  293. const { sql: updateSql } = await pgMeta.tables.update(retrieveRes!, {
  294. name: 'test a',
  295. rls_enabled: true,
  296. rls_forced: true,
  297. replica_identity: 'NOTHING',
  298. comment: 'foo',
  299. })
  300. await executeQuery(updateSql)
  301. // Retrieve the updated table
  302. const { sql: retrieveUpdatedSql, zod: retrieveUpdatedZod } = await pgMeta.tables.retrieve({
  303. name: 'test a',
  304. schema: 'public',
  305. })
  306. const updateRes = retrieveUpdatedZod.parse((await executeQuery(retrieveUpdatedSql))[0])
  307. expect(updateRes).toMatchInlineSnapshot(
  308. {
  309. bytes: expect.any(Number),
  310. dead_rows_estimate: expect.any(Number),
  311. id: expect.any(Number),
  312. live_rows_estimate: expect.any(Number),
  313. size: expect.any(String),
  314. },
  315. `
  316. {
  317. "bytes": Any<Number>,
  318. "columns": [],
  319. "comment": "foo",
  320. "dead_rows_estimate": Any<Number>,
  321. "id": Any<Number>,
  322. "live_rows_estimate": Any<Number>,
  323. "name": "test a",
  324. "primary_keys": [],
  325. "relationships": [],
  326. "replica_identity": "NOTHING",
  327. "rls_enabled": true,
  328. "rls_forced": true,
  329. "schema": "public",
  330. "size": Any<String>,
  331. }
  332. `
  333. )
  334. // Remove table
  335. const { sql: removeSql } = await pgMeta.tables.remove(updateRes!)
  336. await executeQuery(removeSql)
  337. // Verify table is deleted
  338. const { sql: verifyDeleteSql } = await pgMeta.tables.retrieve(updateRes!)
  339. const verifyDeleteRes = await executeQuery(verifyDeleteSql)
  340. expect(verifyDeleteRes).toHaveLength(0)
  341. })
  342. withTestDatabase('update with name unchanged', async ({ executeQuery }) => {
  343. // Create table
  344. const { sql: createSql } = await pgMeta.tables.create({ name: 't' })
  345. await executeQuery(createSql)
  346. // Get the created table
  347. const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.tables.retrieve({
  348. name: 't',
  349. schema: 'public',
  350. })
  351. const table = retrieveZod.parse((await executeQuery(retrieveSql))[0])
  352. // Update table with same name
  353. const { sql: updateSql } = await pgMeta.tables.update(table!, { name: 't' })
  354. await executeQuery(updateSql)
  355. // Verify update
  356. const { sql: verifySQL, zod: verifyZod } = await pgMeta.tables.retrieve({
  357. name: 't',
  358. schema: 'public',
  359. })
  360. const res = verifyZod.parse((await executeQuery(verifySQL))[0])
  361. expect(res).toMatchInlineSnapshot(
  362. {
  363. bytes: expect.any(Number),
  364. dead_rows_estimate: expect.any(Number),
  365. id: expect.any(Number),
  366. live_rows_estimate: expect.any(Number),
  367. size: expect.any(String),
  368. },
  369. `
  370. {
  371. "bytes": Any<Number>,
  372. "columns": [],
  373. "comment": null,
  374. "dead_rows_estimate": Any<Number>,
  375. "id": Any<Number>,
  376. "live_rows_estimate": Any<Number>,
  377. "name": "t",
  378. "primary_keys": [],
  379. "relationships": [],
  380. "replica_identity": "DEFAULT",
  381. "rls_enabled": false,
  382. "rls_forced": false,
  383. "schema": "public",
  384. "size": Any<String>,
  385. }
  386. `
  387. )
  388. })
  389. withTestDatabase("allow ' in comments", async ({ executeQuery }) => {
  390. // Create table with single quote in comment
  391. const { sql: createSql } = await pgMeta.tables.create({
  392. name: 't',
  393. comment: "'",
  394. })
  395. await executeQuery(createSql)
  396. // Verify creation
  397. const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.tables.retrieve({
  398. name: 't',
  399. schema: 'public',
  400. })
  401. const res = retrieveZod.parse((await executeQuery(retrieveSql))[0])
  402. expect(res).toMatchInlineSnapshot(
  403. {
  404. bytes: expect.any(Number),
  405. dead_rows_estimate: expect.any(Number),
  406. id: expect.any(Number),
  407. live_rows_estimate: expect.any(Number),
  408. size: expect.any(String),
  409. },
  410. `
  411. {
  412. "bytes": Any<Number>,
  413. "columns": [],
  414. "comment": "'",
  415. "dead_rows_estimate": Any<Number>,
  416. "id": Any<Number>,
  417. "live_rows_estimate": Any<Number>,
  418. "name": "t",
  419. "primary_keys": [],
  420. "relationships": [],
  421. "replica_identity": "DEFAULT",
  422. "rls_enabled": false,
  423. "rls_forced": false,
  424. "schema": "public",
  425. "size": Any<String>,
  426. }
  427. `
  428. )
  429. })
  430. withTestDatabase('primary keys', async ({ executeQuery }) => {
  431. // Create table with columns
  432. const { sql: createSql } = await pgMeta.tables.create({ name: 't' })
  433. await executeQuery(createSql)
  434. await executeQuery(`
  435. ALTER TABLE t
  436. ADD COLUMN c bigint,
  437. ADD COLUMN cc text
  438. `)
  439. // Get the created table
  440. const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.tables.retrieve({
  441. name: 't',
  442. schema: 'public',
  443. })
  444. const table = retrieveZod.parse((await executeQuery(retrieveSql))[0])
  445. // Update table with primary keys
  446. const { sql: updateSql } = await pgMeta.tables.update(table!, {
  447. primary_keys: [{ name: 'c' }, { name: 'cc' }],
  448. })
  449. await executeQuery(updateSql)
  450. // Verify update
  451. const { sql: verifySQL, zod: verifyZod } = await pgMeta.tables.retrieve({
  452. name: 't',
  453. schema: 'public',
  454. })
  455. const res = verifyZod.parse((await executeQuery(verifySQL))[0])
  456. expect(cleanNondet(res)).toMatchInlineSnapshot(
  457. {
  458. bytes: expect.any(Number),
  459. dead_rows_estimate: expect.any(Number),
  460. id: expect.any(Number),
  461. live_rows_estimate: expect.any(Number),
  462. size: expect.any(String),
  463. },
  464. `
  465. {
  466. "bytes": Any<Number>,
  467. "columns": [
  468. {
  469. "check": null,
  470. "comment": null,
  471. "data_type": "bigint",
  472. "default_value": null,
  473. "enums": [],
  474. "format": "int8",
  475. "format_schema": "pg_catalog",
  476. "identity_generation": null,
  477. "is_generated": false,
  478. "is_identity": false,
  479. "is_nullable": false,
  480. "is_unique": false,
  481. "is_updatable": true,
  482. "name": "c",
  483. "ordinal_position": 1,
  484. "schema": "public",
  485. "table": "t",
  486. },
  487. {
  488. "check": null,
  489. "comment": null,
  490. "data_type": "text",
  491. "default_value": null,
  492. "enums": [],
  493. "format": "text",
  494. "format_schema": "pg_catalog",
  495. "identity_generation": null,
  496. "is_generated": false,
  497. "is_identity": false,
  498. "is_nullable": false,
  499. "is_unique": false,
  500. "is_updatable": true,
  501. "name": "cc",
  502. "ordinal_position": 2,
  503. "schema": "public",
  504. "table": "t",
  505. },
  506. ],
  507. "comment": null,
  508. "dead_rows_estimate": Any<Number>,
  509. "id": Any<Number>,
  510. "live_rows_estimate": Any<Number>,
  511. "name": "t",
  512. "primary_keys": [
  513. {
  514. "name": "c",
  515. "schema": "public",
  516. "table_name": "t",
  517. },
  518. {
  519. "name": "cc",
  520. "schema": "public",
  521. "table_name": "t",
  522. },
  523. ],
  524. "relationships": [],
  525. "replica_identity": "DEFAULT",
  526. "rls_enabled": false,
  527. "rls_forced": false,
  528. "schema": "public",
  529. "size": Any<String>,
  530. }
  531. `
  532. )
  533. })
  534. // /** Additional tests */
  535. withTestDatabase('retrieve table by id', async ({ executeQuery }) => {
  536. const { sql, zod } = await pgMeta.tables.retrieve({ id: 16510 })
  537. const res = zod.parse((await executeQuery(sql))[0])
  538. expect(res).toMatchInlineSnapshot(
  539. {
  540. bytes: expect.any(Number),
  541. dead_rows_estimate: expect.any(Number),
  542. id: expect.any(Number),
  543. live_rows_estimate: expect.any(Number),
  544. size: expect.any(String),
  545. },
  546. `
  547. {
  548. "bytes": Any<Number>,
  549. "columns": [
  550. {
  551. "check": null,
  552. "comment": null,
  553. "data_type": "text",
  554. "default_value": null,
  555. "enums": [],
  556. "format": "text",
  557. "format_schema": "pg_catalog",
  558. "id": "16510.2",
  559. "identity_generation": null,
  560. "is_generated": false,
  561. "is_identity": false,
  562. "is_nullable": false,
  563. "is_unique": false,
  564. "is_updatable": true,
  565. "name": "name",
  566. "ordinal_position": 2,
  567. "schema": "public",
  568. "table": "memes",
  569. "table_id": 16510,
  570. },
  571. {
  572. "check": null,
  573. "comment": null,
  574. "data_type": "integer",
  575. "default_value": null,
  576. "enums": [],
  577. "format": "int4",
  578. "format_schema": "pg_catalog",
  579. "id": "16510.3",
  580. "identity_generation": null,
  581. "is_generated": false,
  582. "is_identity": false,
  583. "is_nullable": true,
  584. "is_unique": false,
  585. "is_updatable": true,
  586. "name": "category",
  587. "ordinal_position": 3,
  588. "schema": "public",
  589. "table": "memes",
  590. "table_id": 16510,
  591. },
  592. {
  593. "check": null,
  594. "comment": null,
  595. "data_type": "jsonb",
  596. "default_value": null,
  597. "enums": [],
  598. "format": "jsonb",
  599. "format_schema": "pg_catalog",
  600. "id": "16510.4",
  601. "identity_generation": null,
  602. "is_generated": false,
  603. "is_identity": false,
  604. "is_nullable": true,
  605. "is_unique": false,
  606. "is_updatable": true,
  607. "name": "metadata",
  608. "ordinal_position": 4,
  609. "schema": "public",
  610. "table": "memes",
  611. "table_id": 16510,
  612. },
  613. {
  614. "check": null,
  615. "comment": null,
  616. "data_type": "timestamp without time zone",
  617. "default_value": null,
  618. "enums": [],
  619. "format": "timestamp",
  620. "format_schema": "pg_catalog",
  621. "id": "16510.5",
  622. "identity_generation": null,
  623. "is_generated": false,
  624. "is_identity": false,
  625. "is_nullable": false,
  626. "is_unique": false,
  627. "is_updatable": true,
  628. "name": "created_at",
  629. "ordinal_position": 5,
  630. "schema": "public",
  631. "table": "memes",
  632. "table_id": 16510,
  633. },
  634. {
  635. "check": null,
  636. "comment": null,
  637. "data_type": "integer",
  638. "default_value": "nextval('memes_id_seq'::regclass)",
  639. "enums": [],
  640. "format": "int4",
  641. "format_schema": "pg_catalog",
  642. "id": "16510.1",
  643. "identity_generation": null,
  644. "is_generated": false,
  645. "is_identity": false,
  646. "is_nullable": false,
  647. "is_unique": false,
  648. "is_updatable": true,
  649. "name": "id",
  650. "ordinal_position": 1,
  651. "schema": "public",
  652. "table": "memes",
  653. "table_id": 16510,
  654. },
  655. {
  656. "check": null,
  657. "comment": null,
  658. "data_type": "USER-DEFINED",
  659. "default_value": "'old'::meme_status",
  660. "enums": [
  661. "new",
  662. "old",
  663. "retired",
  664. ],
  665. "format": "meme_status",
  666. "format_schema": "public",
  667. "id": "16510.6",
  668. "identity_generation": null,
  669. "is_generated": false,
  670. "is_identity": false,
  671. "is_nullable": true,
  672. "is_unique": false,
  673. "is_updatable": true,
  674. "name": "status",
  675. "ordinal_position": 6,
  676. "schema": "public",
  677. "table": "memes",
  678. "table_id": 16510,
  679. },
  680. ],
  681. "comment": null,
  682. "dead_rows_estimate": Any<Number>,
  683. "id": Any<Number>,
  684. "live_rows_estimate": Any<Number>,
  685. "name": "memes",
  686. "primary_keys": [
  687. {
  688. "name": "id",
  689. "schema": "public",
  690. "table_id": 16510,
  691. "table_name": "memes",
  692. },
  693. ],
  694. "relationships": [
  695. {
  696. "constraint_name": "memes_category_fkey",
  697. "id": 16519,
  698. "source_column_name": "category",
  699. "source_schema": "public",
  700. "source_table_name": "memes",
  701. "target_column_name": "id",
  702. "target_table_name": "category",
  703. "target_table_schema": "public",
  704. },
  705. ],
  706. "replica_identity": "DEFAULT",
  707. "rls_enabled": false,
  708. "rls_forced": false,
  709. "schema": "public",
  710. "size": Any<String>,
  711. }
  712. `
  713. )
  714. })
  715. withTestDatabase('retrieve table by name and schema', async ({ executeQuery }) => {
  716. const { sql, zod } = await pgMeta.tables.retrieve({ name: 'memes', schema: 'public' })
  717. const res = zod.parse((await executeQuery(sql))[0])
  718. expect(res).toMatchInlineSnapshot(
  719. {
  720. bytes: expect.any(Number),
  721. dead_rows_estimate: expect.any(Number),
  722. id: expect.any(Number),
  723. live_rows_estimate: expect.any(Number),
  724. size: expect.any(String),
  725. },
  726. `
  727. {
  728. "bytes": Any<Number>,
  729. "columns": [
  730. {
  731. "check": null,
  732. "comment": null,
  733. "data_type": "text",
  734. "default_value": null,
  735. "enums": [],
  736. "format": "text",
  737. "format_schema": "pg_catalog",
  738. "id": "16510.2",
  739. "identity_generation": null,
  740. "is_generated": false,
  741. "is_identity": false,
  742. "is_nullable": false,
  743. "is_unique": false,
  744. "is_updatable": true,
  745. "name": "name",
  746. "ordinal_position": 2,
  747. "schema": "public",
  748. "table": "memes",
  749. "table_id": 16510,
  750. },
  751. {
  752. "check": null,
  753. "comment": null,
  754. "data_type": "integer",
  755. "default_value": null,
  756. "enums": [],
  757. "format": "int4",
  758. "format_schema": "pg_catalog",
  759. "id": "16510.3",
  760. "identity_generation": null,
  761. "is_generated": false,
  762. "is_identity": false,
  763. "is_nullable": true,
  764. "is_unique": false,
  765. "is_updatable": true,
  766. "name": "category",
  767. "ordinal_position": 3,
  768. "schema": "public",
  769. "table": "memes",
  770. "table_id": 16510,
  771. },
  772. {
  773. "check": null,
  774. "comment": null,
  775. "data_type": "jsonb",
  776. "default_value": null,
  777. "enums": [],
  778. "format": "jsonb",
  779. "format_schema": "pg_catalog",
  780. "id": "16510.4",
  781. "identity_generation": null,
  782. "is_generated": false,
  783. "is_identity": false,
  784. "is_nullable": true,
  785. "is_unique": false,
  786. "is_updatable": true,
  787. "name": "metadata",
  788. "ordinal_position": 4,
  789. "schema": "public",
  790. "table": "memes",
  791. "table_id": 16510,
  792. },
  793. {
  794. "check": null,
  795. "comment": null,
  796. "data_type": "timestamp without time zone",
  797. "default_value": null,
  798. "enums": [],
  799. "format": "timestamp",
  800. "format_schema": "pg_catalog",
  801. "id": "16510.5",
  802. "identity_generation": null,
  803. "is_generated": false,
  804. "is_identity": false,
  805. "is_nullable": false,
  806. "is_unique": false,
  807. "is_updatable": true,
  808. "name": "created_at",
  809. "ordinal_position": 5,
  810. "schema": "public",
  811. "table": "memes",
  812. "table_id": 16510,
  813. },
  814. {
  815. "check": null,
  816. "comment": null,
  817. "data_type": "integer",
  818. "default_value": "nextval('memes_id_seq'::regclass)",
  819. "enums": [],
  820. "format": "int4",
  821. "format_schema": "pg_catalog",
  822. "id": "16510.1",
  823. "identity_generation": null,
  824. "is_generated": false,
  825. "is_identity": false,
  826. "is_nullable": false,
  827. "is_unique": false,
  828. "is_updatable": true,
  829. "name": "id",
  830. "ordinal_position": 1,
  831. "schema": "public",
  832. "table": "memes",
  833. "table_id": 16510,
  834. },
  835. {
  836. "check": null,
  837. "comment": null,
  838. "data_type": "USER-DEFINED",
  839. "default_value": "'old'::meme_status",
  840. "enums": [
  841. "new",
  842. "old",
  843. "retired",
  844. ],
  845. "format": "meme_status",
  846. "format_schema": "public",
  847. "id": "16510.6",
  848. "identity_generation": null,
  849. "is_generated": false,
  850. "is_identity": false,
  851. "is_nullable": true,
  852. "is_unique": false,
  853. "is_updatable": true,
  854. "name": "status",
  855. "ordinal_position": 6,
  856. "schema": "public",
  857. "table": "memes",
  858. "table_id": 16510,
  859. },
  860. ],
  861. "comment": null,
  862. "dead_rows_estimate": Any<Number>,
  863. "id": Any<Number>,
  864. "live_rows_estimate": Any<Number>,
  865. "name": "memes",
  866. "primary_keys": [
  867. {
  868. "name": "id",
  869. "schema": "public",
  870. "table_id": 16510,
  871. "table_name": "memes",
  872. },
  873. ],
  874. "relationships": [
  875. {
  876. "constraint_name": "memes_category_fkey",
  877. "id": 16519,
  878. "source_column_name": "category",
  879. "source_schema": "public",
  880. "source_table_name": "memes",
  881. "target_column_name": "id",
  882. "target_table_name": "category",
  883. "target_table_schema": "public",
  884. },
  885. ],
  886. "replica_identity": "DEFAULT",
  887. "rls_enabled": false,
  888. "rls_forced": false,
  889. "schema": "public",
  890. "size": Any<String>,
  891. }
  892. `
  893. )
  894. })
  895. withTestDatabase('retrieve error if missing identifiers', async ({}) => {
  896. await expect(async () => {
  897. //@ts-expect-error use with missing params
  898. await pgMeta.tables.retrieve({ name: 'memes' })
  899. }).rejects.toThrow('Must provide either id or name and schema')
  900. })
  901. withTestDatabase('remove table by id', async ({ executeQuery }) => {
  902. // First create a test table
  903. await executeQuery(`
  904. CREATE TABLE test_remove_table (
  905. id SERIAL PRIMARY KEY,
  906. name TEXT
  907. );
  908. `)
  909. // Get the table's id
  910. const { sql: listSql, zod: listZod } = await pgMeta.tables.list()
  911. const tables = listZod.parse(await executeQuery(listSql))
  912. const tableId = tables.find((t) => t.name === 'test_remove_table')!
  913. // Remove the table
  914. const { sql } = await pgMeta.tables.remove(tableId)
  915. await executeQuery(sql)
  916. // Verify the table is gone
  917. const tablesAfter = listZod.parse(await executeQuery(listSql))
  918. expect(tablesAfter.find((t) => t.name === 'test_remove_table')).toBeUndefined()
  919. })
  920. withTestDatabase('remove table by name and schema', async ({ executeQuery }) => {
  921. // First create a test table
  922. await executeQuery(`
  923. CREATE TABLE test_remove_table_2 (
  924. id SERIAL PRIMARY KEY,
  925. name TEXT
  926. );
  927. `)
  928. // Remove the table
  929. const { sql } = await pgMeta.tables.remove({
  930. name: 'test_remove_table_2',
  931. schema: 'public',
  932. })
  933. await executeQuery(sql)
  934. // Verify the table is gone
  935. const { sql: listSql, zod: listZod } = await pgMeta.tables.list()
  936. const tables = listZod.parse(await executeQuery(listSql))
  937. expect(tables.find((t) => t.name === 'test_remove_table_2')).toBeUndefined()
  938. })
  939. withTestDatabase('remove throws error for non-existent table', async ({ executeQuery }) => {
  940. const { sql } = await pgMeta.tables.remove({
  941. name: 'non_existent_table',
  942. schema: 'public',
  943. })
  944. // With schema and name
  945. await expect(executeQuery(sql)).rejects.toThrow(
  946. `Failed to execute query: table "non_existent_table" does not exist`
  947. )
  948. })
  949. withTestDatabase('remove throws error with missing identifiers', async ({}) => {
  950. await expect(async () => {
  951. //@ts-expect-error use with missing params
  952. await pgMeta.tables.remove({ name: 'some_table' })
  953. }).rejects.toThrow('SQL identifier cannot be null or undefined')
  954. })
  955. withTestDatabase('update table - rename', async ({ executeQuery }) => {
  956. // Create test table
  957. const { sql: createSql } = await pgMeta.tables.create({ name: 'test_rename' })
  958. await executeQuery(createSql)
  959. // Update table name
  960. const { sql: updateSql } = await pgMeta.tables.update(
  961. { id: 0, name: 'test_rename', schema: 'public' },
  962. { name: 'test_renamed' }
  963. )
  964. await executeQuery(updateSql)
  965. // Verify update
  966. const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
  967. name: 'test_renamed',
  968. schema: 'public',
  969. })
  970. const res = zod.parse((await executeQuery(retrieveSql))[0])
  971. expect(res!.name).toBe('test_renamed')
  972. })
  973. withTestDatabase('update table - change schema', async ({ executeQuery }) => {
  974. // Create test schema and table
  975. await executeQuery('CREATE SCHEMA test_schema')
  976. const { sql: createSql } = await pgMeta.tables.create({ name: 'test_schema_move' })
  977. await executeQuery(createSql)
  978. // Move table to new schema
  979. const { sql: updateSql } = await pgMeta.tables.update(
  980. { id: 0, name: 'test_schema_move', schema: 'public' },
  981. { schema: 'test_schema' }
  982. )
  983. await executeQuery(updateSql)
  984. // Verify update
  985. const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
  986. name: 'test_schema_move',
  987. schema: 'test_schema',
  988. })
  989. const res = zod.parse((await executeQuery(retrieveSql))[0])
  990. expect(res!.schema).toBe('test_schema')
  991. })
  992. withTestDatabase('update table - row level security', async ({ executeQuery }) => {
  993. // Create test table
  994. const { sql: createSql } = await pgMeta.tables.create({ name: 'test_rls' })
  995. await executeQuery(createSql)
  996. // Enable RLS
  997. const { sql: updateSql } = await pgMeta.tables.update(
  998. { id: 0, name: 'test_rls', schema: 'public' },
  999. { rls_enabled: true, rls_forced: true }
  1000. )
  1001. await executeQuery(updateSql)
  1002. // Verify update
  1003. const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
  1004. name: 'test_rls',
  1005. schema: 'public',
  1006. })
  1007. const res = zod.parse((await executeQuery(retrieveSql))[0])
  1008. expect(res!.rls_enabled).toBe(true)
  1009. expect(res!.rls_forced).toBe(true)
  1010. })
  1011. withTestDatabase('update table - replica identity', async ({ executeQuery }) => {
  1012. // Create test table
  1013. const { sql: createSql } = await pgMeta.tables.create({ name: 'test_replica' })
  1014. await executeQuery(createSql)
  1015. // Change replica identity
  1016. const { sql: updateSql } = await pgMeta.tables.update(
  1017. { id: 0, name: 'test_replica', schema: 'public' },
  1018. { replica_identity: 'NOTHING' }
  1019. )
  1020. await executeQuery(updateSql)
  1021. // Verify update
  1022. const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
  1023. name: 'test_replica',
  1024. schema: 'public',
  1025. })
  1026. const res = zod.parse((await executeQuery(retrieveSql))[0])
  1027. expect(res!.replica_identity).toBe('NOTHING')
  1028. })
  1029. withTestDatabase('update table - replica identity INDEX requires index name', async () => {
  1030. expect(() =>
  1031. pgMeta.tables.update({ id: 1, name: 'test', schema: 'public' }, { replica_identity: 'INDEX' })
  1032. ).toThrow('replica_identity_index is required')
  1033. })
  1034. withTestDatabase('update table - primary keys', async ({ executeQuery }) => {
  1035. // Create test table with a column
  1036. const { sql: createSql } = await pgMeta.tables.create({ name: 'test_pk' })
  1037. await executeQuery(createSql)
  1038. await executeQuery('ALTER TABLE test_pk ADD COLUMN id INT')
  1039. // Add primary key
  1040. const { sql: updateSql } = await pgMeta.tables.update(
  1041. { id: 0, name: 'test_pk', schema: 'public' },
  1042. { primary_keys: [{ name: 'id' }] }
  1043. )
  1044. await executeQuery(updateSql)
  1045. // Verify update
  1046. const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
  1047. name: 'test_pk',
  1048. schema: 'public',
  1049. })
  1050. const res = zod.parse((await executeQuery(retrieveSql))[0])
  1051. expect(res!.primary_keys).toHaveLength(1)
  1052. expect(res!.primary_keys[0].name).toBe('id')
  1053. })
  1054. withTestDatabase('update table - remove primary keys', async ({ executeQuery }) => {
  1055. // Create test table with primary key
  1056. const { sql: createSql } = await pgMeta.tables.create({ name: 'test_pk_remove' })
  1057. await executeQuery(createSql)
  1058. await executeQuery('ALTER TABLE test_pk_remove ADD COLUMN id INT PRIMARY KEY')
  1059. const { sql: tableSql, zod: tableZod } = await pgMeta.tables.retrieve({
  1060. name: 'test_pk_remove',
  1061. schema: 'public',
  1062. })
  1063. const table = tableZod.parse((await executeQuery(tableSql))[0])
  1064. // Remove primary key
  1065. const { sql: updateSql } = await pgMeta.tables.update(table!, { primary_keys: [] })
  1066. await executeQuery(updateSql)
  1067. // Verify update
  1068. const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
  1069. name: 'test_pk_remove',
  1070. schema: 'public',
  1071. })
  1072. const res = zod.parse((await executeQuery(retrieveSql))[0])
  1073. expect(res!.primary_keys).toHaveLength(0)
  1074. })
  1075. withTestDatabase('update table - comment', async ({ executeQuery }) => {
  1076. // Create test table
  1077. const { sql: createSql } = await pgMeta.tables.create({ name: 'test_comment' })
  1078. await executeQuery(createSql)
  1079. // Add comment
  1080. const { sql: updateSql } = await pgMeta.tables.update(
  1081. { id: 0, name: 'test_comment', schema: 'public' },
  1082. { comment: 'Test comment' }
  1083. )
  1084. await executeQuery(updateSql)
  1085. // Verify update
  1086. const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
  1087. name: 'test_comment',
  1088. schema: 'public',
  1089. })
  1090. const res = zod.parse((await executeQuery(retrieveSql))[0])
  1091. expect(res!.comment).toBe('Test comment')
  1092. })
  1093. withTestDatabase('update table - multiple changes', async ({ executeQuery }) => {
  1094. // Create test table
  1095. const { sql: createSql } = await pgMeta.tables.create({ name: 'test_multiple' })
  1096. await executeQuery(createSql)
  1097. await executeQuery('ALTER TABLE test_multiple ADD COLUMN id INT')
  1098. // Make multiple changes
  1099. const { sql: updateSql } = await pgMeta.tables.update(
  1100. { id: 0, name: 'test_multiple', schema: 'public' },
  1101. {
  1102. name: 'test_multiple_updated',
  1103. comment: 'Updated table',
  1104. rls_enabled: true,
  1105. primary_keys: [{ name: 'id' }],
  1106. replica_identity: 'FULL',
  1107. }
  1108. )
  1109. await executeQuery(updateSql)
  1110. // Verify all updates
  1111. const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
  1112. name: 'test_multiple_updated',
  1113. schema: 'public',
  1114. })
  1115. const res = zod.parse((await executeQuery(retrieveSql))[0])
  1116. expect(res).toMatchObject({
  1117. name: 'test_multiple_updated',
  1118. comment: 'Updated table',
  1119. rls_enabled: true,
  1120. replica_identity: 'FULL',
  1121. primary_keys: [expect.objectContaining({ name: 'id' })],
  1122. })
  1123. })
  1124. withTestDatabase('update table - by id', async ({ executeQuery }) => {
  1125. // Create test table
  1126. const { sql: createSql } = await pgMeta.tables.create({ name: 'test_by_id' })
  1127. await executeQuery(createSql)
  1128. // Get table id
  1129. const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
  1130. name: 'test_by_id',
  1131. schema: 'public',
  1132. })
  1133. const table = zod.parse((await executeQuery(retrieveSql))[0])
  1134. // Update by id
  1135. const { sql: updateSql } = await pgMeta.tables.update(table!, { name: 'test_by_id_updated' })
  1136. await executeQuery(updateSql)
  1137. // Verify update
  1138. const { sql: verifySQL, zod: verifyZod } = await pgMeta.tables.retrieve({
  1139. name: 'test_by_id_updated',
  1140. schema: 'public',
  1141. })
  1142. const res = verifyZod.parse((await executeQuery(verifySQL))[0])
  1143. expect(res!.name).toBe('test_by_id_updated')
  1144. })
  1145. withTestDatabase('update table - error on non-existent table', async ({ executeQuery }) => {
  1146. const { sql: updateSql } = await pgMeta.tables.update(
  1147. { id: 0, name: 'non_existent', schema: 'public' },
  1148. { name: 'new_name' }
  1149. )
  1150. await expect(executeQuery(updateSql)).rejects.toThrow()
  1151. })
  1152. withTestDatabase('update table - rename with schema change', async ({ executeQuery }) => {
  1153. // Create test schema and table
  1154. await executeQuery('CREATE SCHEMA test_schema')
  1155. const { sql: createSql } = await pgMeta.tables.create({ name: 'test_rename_schema' })
  1156. await executeQuery(createSql)
  1157. // Update both name and schema
  1158. const { sql: updateSql } = await pgMeta.tables.update(
  1159. { id: 0, name: 'test_rename_schema', schema: 'public' },
  1160. {
  1161. name: 'test_renamed_schema',
  1162. schema: 'test_schema',
  1163. }
  1164. )
  1165. await executeQuery(updateSql)
  1166. // Verify update
  1167. const { sql: retrieveSql, zod } = await pgMeta.tables.retrieve({
  1168. name: 'test_renamed_schema',
  1169. schema: 'test_schema',
  1170. })
  1171. const res = zod.parse((await executeQuery(retrieveSql))[0])
  1172. expect(res!.name).toBe('test_renamed_schema')
  1173. expect(res!.schema).toBe('test_schema')
  1174. })
  1175. // Table creation test cases
  1176. const tableCreationTests = [
  1177. {
  1178. name: 'create table with default schema',
  1179. input: {
  1180. name: 'test_table_1',
  1181. },
  1182. expectedSchema: 'public',
  1183. expectedComment: null,
  1184. },
  1185. {
  1186. name: 'create table with explicit public schema',
  1187. input: {
  1188. name: 'test_table_2',
  1189. schema: 'public',
  1190. },
  1191. expectedSchema: 'public',
  1192. expectedComment: null,
  1193. },
  1194. {
  1195. name: 'create table with custom schema',
  1196. input: {
  1197. name: 'test_table_3',
  1198. schema: 'custom_schema',
  1199. },
  1200. expectedSchema: 'custom_schema',
  1201. expectedComment: null,
  1202. beforeTest: async (
  1203. executeQuery: Awaited<ReturnType<typeof createTestDatabase>>['executeQuery']
  1204. ) => {
  1205. await executeQuery('CREATE SCHEMA IF NOT EXISTS custom_schema')
  1206. },
  1207. },
  1208. {
  1209. name: 'create table with comment',
  1210. input: {
  1211. name: 'test_table_4',
  1212. comment: 'Test comment',
  1213. },
  1214. expectedSchema: 'public',
  1215. expectedComment: 'Test comment',
  1216. },
  1217. {
  1218. name: 'create table with empty string comment',
  1219. input: {
  1220. name: 'test_table_5',
  1221. comment: '',
  1222. },
  1223. expectedSchema: 'public',
  1224. expectedComment: null, // PostgreSQL treats empty string comments as NULL
  1225. },
  1226. {
  1227. name: 'create table with null comment',
  1228. input: {
  1229. name: 'test_table_6',
  1230. comment: null,
  1231. },
  1232. expectedSchema: 'public',
  1233. expectedComment: null,
  1234. },
  1235. {
  1236. name: 'create table with uppercase name',
  1237. input: {
  1238. name: 'UPPERCASE_TABLE',
  1239. },
  1240. expectedSchema: 'public',
  1241. expectedComment: null,
  1242. },
  1243. {
  1244. name: 'create table with mixed case name',
  1245. input: {
  1246. name: 'MixedCase_Table_Name',
  1247. },
  1248. expectedSchema: 'public',
  1249. expectedComment: null,
  1250. },
  1251. {
  1252. name: 'create table with quoted name',
  1253. input: {
  1254. name: 'table "with" quotes',
  1255. },
  1256. expectedSchema: 'public',
  1257. expectedComment: null,
  1258. },
  1259. {
  1260. name: 'create table with special characters',
  1261. input: {
  1262. name: 'table$with#special@chars',
  1263. },
  1264. expectedSchema: 'public',
  1265. expectedComment: null,
  1266. },
  1267. {
  1268. name: 'create table with name at maximum length',
  1269. input: {
  1270. name: 'a'.repeat(63), // PostgreSQL has a 63-byte limit for identifiers
  1271. },
  1272. expectedSchema: 'public',
  1273. expectedComment: null,
  1274. },
  1275. {
  1276. name: 'create table with schema containing special characters',
  1277. input: {
  1278. name: 'normal_table',
  1279. schema: 'Special.Schema$Name',
  1280. },
  1281. expectedSchema: 'Special.Schema$Name',
  1282. expectedComment: null,
  1283. beforeTest: async (executeQuery: TestDb['executeQuery']) => {
  1284. await executeQuery('CREATE SCHEMA IF NOT EXISTS "Special.Schema$Name"')
  1285. },
  1286. },
  1287. {
  1288. name: 'create table with very long name',
  1289. input: {
  1290. name: 'a'.repeat(63),
  1291. },
  1292. expectedSchema: 'public',
  1293. expectedComment: null,
  1294. },
  1295. {
  1296. name: 'create table with special characters in name',
  1297. input: {
  1298. name: 'table,name',
  1299. },
  1300. expectedSchema: 'public',
  1301. expectedComment: null,
  1302. },
  1303. ]
  1304. // SQL injection test cases
  1305. const sqlInjectionTests = [
  1306. {
  1307. name: 'prevent SQL injection in table name',
  1308. input: {
  1309. name: "table_name'; DROP TABLE users; --",
  1310. },
  1311. },
  1312. {
  1313. name: 'prevent SQL injection in comment',
  1314. input: {
  1315. name: 'safe_table',
  1316. comment: "normal comment'; DROP TABLE users; --",
  1317. },
  1318. },
  1319. ]
  1320. // Error test cases
  1321. const errorTests = [
  1322. {
  1323. name: 'fail on duplicate table name',
  1324. input: { name: 'duplicate_table' },
  1325. setup: async (executeQuery: TestDb['executeQuery']) => {
  1326. await executeQuery(pgMeta.tables.create({ name: 'duplicate_table' }).sql)
  1327. },
  1328. expectedError: /relation.*already exists/,
  1329. },
  1330. {
  1331. name: 'fail on invalid schema',
  1332. input: { name: 'test_table', schema: 'nonexistent_schema' },
  1333. expectedError: /schema.*does not exist/,
  1334. },
  1335. {
  1336. name: 'fail on empty table name',
  1337. input: {
  1338. name: '',
  1339. },
  1340. expectedError: /zero-length delimited identifier/,
  1341. },
  1342. {
  1343. name: 'fail on schema name exceeding maximum length',
  1344. input: {
  1345. name: 'table',
  1346. schema: 'a'.repeat(64),
  1347. },
  1348. expectedError: /schema.*does not exist/,
  1349. },
  1350. ]
  1351. // Generate individual test cases for successful table creation
  1352. for (const testCase of tableCreationTests) {
  1353. withTestDatabase(testCase.name, async ({ executeQuery }) => {
  1354. if (testCase.beforeTest) {
  1355. await testCase.beforeTest(executeQuery)
  1356. }
  1357. const { sql } = pgMeta.tables.create(testCase.input)
  1358. await executeQuery(sql)
  1359. const { sql: listSql, zod: listZod } = await pgMeta.tables.list()
  1360. const tables = listZod.parse(await executeQuery(listSql))
  1361. const createdTable = tables.find((t) => t.name === testCase.input.name)
  1362. expect(createdTable).toBeDefined()
  1363. expect(createdTable?.schema).toBe(testCase.expectedSchema)
  1364. expect(createdTable?.comment).toBe(testCase.expectedComment)
  1365. })
  1366. }
  1367. // Generate individual test cases for SQL injection prevention
  1368. for (const testCase of sqlInjectionTests) {
  1369. withTestDatabase(`create - ${testCase.name}`, async ({ executeQuery }) => {
  1370. const { sql } = pgMeta.tables.create(testCase.input)
  1371. await executeQuery(sql)
  1372. // Verify table was created with correct name
  1373. const { sql: listSql, zod: listZod } = await pgMeta.tables.list()
  1374. const tables = listZod.parse(await executeQuery(listSql))
  1375. expect(tables.find((t) => t.name === testCase.input.name)).toBeDefined()
  1376. // Verify users table still exists (wasn't dropped)
  1377. expect(tables.find((t) => t.name === 'users')).toBeDefined()
  1378. })
  1379. }
  1380. // Generate individual test cases for error conditions
  1381. for (const testCase of errorTests) {
  1382. withTestDatabase(`create - ${testCase.name}`, async ({ executeQuery }) => {
  1383. if (testCase.setup) {
  1384. await testCase.setup(executeQuery)
  1385. }
  1386. const { sql } = pgMeta.tables.create(testCase.input)
  1387. await expect(executeQuery(sql)).rejects.toThrow(testCase.expectedError)
  1388. })
  1389. }