columns.test.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967
  1. import { afterAll, beforeAll, describe, expect, test } from 'vitest'
  2. import pgMeta from '../src/index'
  3. import { rawSql } from '../src/pg-format'
  4. import { cleanupRoot, createTestDatabase } from './db/utils'
  5. beforeAll(async () => {
  6. // Any global setup if needed
  7. })
  8. afterAll(async () => {
  9. await cleanupRoot()
  10. })
  11. const withTestDatabase = (
  12. name: string,
  13. fn: (db: Awaited<ReturnType<typeof createTestDatabase>>) => Promise<void>
  14. ) => {
  15. test(name, async () => {
  16. const db = await createTestDatabase()
  17. try {
  18. await fn(db)
  19. } finally {
  20. await db.cleanup()
  21. }
  22. })
  23. }
  24. withTestDatabase('list columns', async ({ executeQuery }) => {
  25. const { sql, zod } = await pgMeta.columns.list()
  26. const res = zod.parse(await executeQuery(sql))
  27. const userIdColumn = res.find(({ name }) => name === 'user-id')
  28. expect(userIdColumn).toMatchInlineSnapshot(
  29. {
  30. id: expect.stringMatching(/^\d+\.3$/),
  31. table_id: expect.any(Number),
  32. },
  33. `
  34. {
  35. "check": null,
  36. "comment": null,
  37. "data_type": "bigint",
  38. "default_value": null,
  39. "enums": [],
  40. "format": "int8",
  41. "format_schema": "pg_catalog",
  42. "id": StringMatching /\\^\\\\d\\+\\\\\\.3\\$/,
  43. "identity_generation": null,
  44. "is_generated": false,
  45. "is_identity": false,
  46. "is_nullable": false,
  47. "is_unique": false,
  48. "is_updatable": true,
  49. "name": "user-id",
  50. "ordinal_position": 3,
  51. "schema": "public",
  52. "table": "todos",
  53. "table_id": Any<Number>,
  54. }
  55. `
  56. )
  57. })
  58. withTestDatabase('list columns from a single table', async ({ executeQuery }) => {
  59. // Create test table
  60. await executeQuery('CREATE TABLE t (c1 text, c2 text)')
  61. // Get the table ID directly
  62. const tableId = Number(
  63. (
  64. await executeQuery(
  65. "SELECT oid FROM pg_class WHERE relname = 't' AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')"
  66. )
  67. )[0].oid
  68. )
  69. const testTable = { id: tableId, name: 't', schema: 'public' }
  70. // List columns
  71. const { sql, zod } = await pgMeta.columns.list({ tableId: testTable.id })
  72. const res = zod.parse(await executeQuery(sql))
  73. expect(res).toMatchInlineSnapshot(
  74. [
  75. {
  76. id: expect.stringMatching(/^\d+\.\d+$/),
  77. table_id: expect.any(Number),
  78. },
  79. {
  80. id: expect.stringMatching(/^\d+\.\d+$/),
  81. table_id: expect.any(Number),
  82. },
  83. ],
  84. `
  85. [
  86. {
  87. "check": null,
  88. "comment": null,
  89. "data_type": "text",
  90. "default_value": null,
  91. "enums": [],
  92. "format": "text",
  93. "format_schema": "pg_catalog",
  94. "id": StringMatching /\\^\\\\d\\+\\\\\\.\\\\d\\+\\$/,
  95. "identity_generation": null,
  96. "is_generated": false,
  97. "is_identity": false,
  98. "is_nullable": true,
  99. "is_unique": false,
  100. "is_updatable": true,
  101. "name": "c1",
  102. "ordinal_position": 1,
  103. "schema": "public",
  104. "table": "t",
  105. "table_id": Any<Number>,
  106. },
  107. {
  108. "check": null,
  109. "comment": null,
  110. "data_type": "text",
  111. "default_value": null,
  112. "enums": [],
  113. "format": "text",
  114. "format_schema": "pg_catalog",
  115. "id": StringMatching /\\^\\\\d\\+\\\\\\.\\\\d\\+\\$/,
  116. "identity_generation": null,
  117. "is_generated": false,
  118. "is_identity": false,
  119. "is_nullable": true,
  120. "is_unique": false,
  121. "is_updatable": true,
  122. "name": "c2",
  123. "ordinal_position": 2,
  124. "schema": "public",
  125. "table": "t",
  126. "table_id": Any<Number>,
  127. },
  128. ]
  129. `
  130. )
  131. })
  132. withTestDatabase('list columns with included schemas', async ({ executeQuery }) => {
  133. const { sql, zod } = await pgMeta.columns.list({
  134. includedSchemas: ['public'],
  135. })
  136. const res = zod.parse(await executeQuery(sql))
  137. expect(res.length).toBeGreaterThan(0)
  138. res.forEach((column) => {
  139. expect(column.schema).toBe('public')
  140. })
  141. })
  142. withTestDatabase('list columns with excluded schemas', async ({ executeQuery }) => {
  143. const { sql, zod } = await pgMeta.columns.list({
  144. excludedSchemas: ['public'],
  145. })
  146. const res = zod.parse(await executeQuery(sql))
  147. res.forEach((column) => {
  148. expect(column.schema).not.toBe('public')
  149. })
  150. })
  151. withTestDatabase(
  152. 'list columns with excluded schemas and include System Schemas',
  153. async ({ executeQuery }) => {
  154. const { sql, zod } = await pgMeta.columns.list({
  155. excludedSchemas: ['public'],
  156. includeSystemSchemas: true,
  157. })
  158. const res = zod.parse(await executeQuery(sql))
  159. expect(res.length).toBeGreaterThan(0)
  160. res.forEach((column) => {
  161. expect(column.schema).not.toBe('public')
  162. })
  163. }
  164. )
  165. withTestDatabase('retrieve, create, update, delete column', async ({ executeQuery }) => {
  166. // Create test table using pure SQL
  167. await executeQuery('CREATE TABLE t ()')
  168. // Create column
  169. const { sql: createColumnSql } = await pgMeta.columns.create({
  170. schema: 'public',
  171. table: 't',
  172. name: 'c',
  173. type: { name: 'int2' },
  174. default_value: 42,
  175. comment: 'foo',
  176. })
  177. await executeQuery(createColumnSql)
  178. // Retrieve and verify created column
  179. const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.columns.retrieve({
  180. schema: 'public',
  181. table: 't',
  182. name: 'c',
  183. })
  184. let column = retrieveZod.parse((await executeQuery(retrieveSql))[0])
  185. expect(column).toMatchInlineSnapshot(
  186. {
  187. id: expect.stringMatching(/^\d+\.1$/),
  188. table_id: expect.any(Number),
  189. },
  190. `
  191. {
  192. "check": null,
  193. "comment": "foo",
  194. "data_type": "smallint",
  195. "default_value": "42",
  196. "enums": [],
  197. "format": "int2",
  198. "format_schema": "pg_catalog",
  199. "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/,
  200. "identity_generation": null,
  201. "is_generated": false,
  202. "is_identity": false,
  203. "is_nullable": true,
  204. "is_unique": false,
  205. "is_updatable": true,
  206. "name": "c",
  207. "ordinal_position": 1,
  208. "schema": "public",
  209. "table": "t",
  210. "table_id": Any<Number>,
  211. }
  212. `
  213. )
  214. // Update column
  215. const { sql: updateSql } = await pgMeta.columns.update(column!, {
  216. name: 'c1',
  217. type: { name: 'int4' },
  218. drop_default: true,
  219. is_identity: true,
  220. identity_generation: 'ALWAYS',
  221. is_nullable: false,
  222. comment: 'bar',
  223. })
  224. await executeQuery(updateSql)
  225. // Verify updated column
  226. const { sql: retrieveUpdatedSql, zod: retrieveUpdatedZod } = await pgMeta.columns.retrieve({
  227. schema: 'public',
  228. table: 't',
  229. name: 'c1',
  230. })
  231. column = retrieveUpdatedZod.parse((await executeQuery(retrieveUpdatedSql))[0])
  232. expect(column).toMatchInlineSnapshot(
  233. {
  234. id: expect.stringMatching(/^\d+\.1$/),
  235. table_id: expect.any(Number),
  236. },
  237. `
  238. {
  239. "check": null,
  240. "comment": "bar",
  241. "data_type": "integer",
  242. "default_value": null,
  243. "enums": [],
  244. "format": "int4",
  245. "format_schema": "pg_catalog",
  246. "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/,
  247. "identity_generation": "ALWAYS",
  248. "is_generated": false,
  249. "is_identity": true,
  250. "is_nullable": false,
  251. "is_unique": false,
  252. "is_updatable": true,
  253. "name": "c1",
  254. "ordinal_position": 1,
  255. "schema": "public",
  256. "table": "t",
  257. "table_id": Any<Number>,
  258. }
  259. `
  260. )
  261. // Remove column
  262. const { sql: removeSql } = await pgMeta.columns.remove(column!)
  263. await executeQuery(removeSql)
  264. // Verify column was removed
  265. const { sql: retrieveRemovedSql, zod: retrieveRemovedZod } = await pgMeta.columns.retrieve({
  266. schema: 'public',
  267. table: 't',
  268. name: 'c1',
  269. })
  270. const removedColumn = retrieveRemovedZod.parse((await executeQuery(retrieveRemovedSql))[0])
  271. expect(removedColumn).toBeUndefined()
  272. })
  273. withTestDatabase('enum column with quoted name', async ({ executeQuery }) => {
  274. await executeQuery('CREATE TYPE "T" AS ENUM (\'v\'); CREATE TABLE t ( c "T" );')
  275. const { sql, zod } = await pgMeta.columns.list()
  276. const res = zod.parse(await executeQuery(sql))
  277. expect(res.find(({ table }) => table === 't')).toMatchInlineSnapshot(
  278. {
  279. id: expect.stringMatching(/^\d+\.1$/),
  280. table_id: expect.any(Number),
  281. },
  282. `
  283. {
  284. "check": null,
  285. "comment": null,
  286. "data_type": "USER-DEFINED",
  287. "default_value": null,
  288. "enums": [
  289. "v",
  290. ],
  291. "format": "T",
  292. "format_schema": "public",
  293. "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/,
  294. "identity_generation": null,
  295. "is_generated": false,
  296. "is_identity": false,
  297. "is_nullable": true,
  298. "is_unique": false,
  299. "is_updatable": true,
  300. "name": "c",
  301. "ordinal_position": 1,
  302. "schema": "public",
  303. "table": "t",
  304. "table_id": Any<Number>,
  305. }
  306. `
  307. )
  308. })
  309. withTestDatabase('primary key column', async ({ executeQuery }) => {
  310. // Create test table using pure SQL
  311. await executeQuery('CREATE TABLE t ()')
  312. // Create column with primary key
  313. const { sql: createColumnSql } = await pgMeta.columns.create({
  314. schema: 'public',
  315. table: 't',
  316. name: 'c',
  317. type: { name: 'int2' },
  318. is_primary_key: true,
  319. })
  320. await executeQuery(createColumnSql)
  321. // Verify primary key using pure SQL
  322. const primaryKeyResult = await executeQuery(`
  323. SELECT a.attname
  324. FROM pg_index i
  325. JOIN pg_attribute a ON a.attrelid = i.indrelid
  326. AND a.attnum = ANY(i.indkey)
  327. WHERE i.indrelid = 't'::regclass
  328. AND i.indisprimary;
  329. `)
  330. expect(primaryKeyResult).toMatchInlineSnapshot(`
  331. [
  332. {
  333. "attname": "c",
  334. },
  335. ]
  336. `)
  337. })
  338. withTestDatabase('unique column', async ({ executeQuery }) => {
  339. // Create test table using pure SQL
  340. await executeQuery('CREATE TABLE t ()')
  341. // Create column with unique constraint
  342. const { sql: createColumnSql } = await pgMeta.columns.create({
  343. schema: 'public',
  344. table: 't',
  345. name: 'c',
  346. type: { name: 'int2' },
  347. is_unique: true,
  348. })
  349. await executeQuery(createColumnSql)
  350. // Verify unique constraint using pure SQL
  351. const uniqueResult = await executeQuery(`
  352. SELECT a.attname
  353. FROM pg_index i
  354. JOIN pg_constraint c ON c.conindid = i.indexrelid
  355. JOIN pg_attribute a ON a.attrelid = i.indrelid
  356. AND a.attnum = ANY(i.indkey)
  357. WHERE i.indrelid = 't'::regclass
  358. AND i.indisunique;
  359. `)
  360. expect(uniqueResult).toMatchInlineSnapshot(`
  361. [
  362. {
  363. "attname": "c",
  364. },
  365. ]
  366. `)
  367. })
  368. describe('array column', async () => {
  369. const db = await createTestDatabase()
  370. await db.executeQuery('CREATE TABLE t ()')
  371. afterAll(async () => {
  372. await db.cleanup()
  373. })
  374. test.concurrent.for([
  375. // numerical types
  376. { type: 'int2', etype: '_int2' },
  377. { type: 'int4', etype: '_int4' },
  378. { type: 'int8', etype: '_int8' },
  379. { type: 'float4', etype: '_float4' },
  380. { type: 'float8', etype: '_float8' },
  381. { type: 'numeric', etype: '_numeric' },
  382. // json types
  383. { type: 'json', etype: '_json' },
  384. { type: 'jsonb', etype: '_jsonb' },
  385. // text types
  386. { type: 'text', etype: '_text' },
  387. { type: 'varchar', etype: '_varchar' },
  388. // datetime types
  389. { type: 'timestamp', etype: '_timestamp' },
  390. { type: 'timestamptz', etype: '_timestamptz' },
  391. { type: 'date', etype: '_date' },
  392. { type: 'time', etype: '_time' },
  393. { type: 'timetz', etype: '_timetz' },
  394. // other types
  395. { type: 'uuid', etype: '_uuid' },
  396. { type: 'bool', etype: '_bool' },
  397. { type: 'bytea', etype: '_bytea' },
  398. ])('$type[] -> $etype', async (c, { expect, task }) => {
  399. const id = { schema: 'public', table: 't', name: `c${task.id}` }
  400. // Create column with default value
  401. const { sql } = pgMeta.columns.create({
  402. ...id,
  403. type: { name: c.type, isArray: true },
  404. default_value: null,
  405. })
  406. await db.executeQuery(sql)
  407. // Retrieve and verify the created column
  408. const expected = pgMeta.columns.retrieve(id)
  409. const result = await db.executeQuery(expected.sql)
  410. const column = expected.zod.parse(result[0])
  411. expect(column).toStrictEqual({
  412. ...id,
  413. data_type: 'ARRAY',
  414. default_value: null,
  415. format: c.etype,
  416. format_schema: 'pg_catalog',
  417. id: expect.stringMatching(/^\d+\.\d+$/),
  418. ordinal_position: expect.any(Number),
  419. table_id: expect.any(Number),
  420. check: null,
  421. comment: null,
  422. enums: [],
  423. identity_generation: null,
  424. is_generated: false,
  425. is_identity: false,
  426. is_nullable: true,
  427. is_unique: false,
  428. is_updatable: true,
  429. })
  430. })
  431. })
  432. describe('column with default value', async () => {
  433. const db = await createTestDatabase()
  434. await db.executeQuery('CREATE TABLE t ()')
  435. afterAll(async () => {
  436. await db.cleanup()
  437. })
  438. test.concurrent.for([
  439. // numerical types
  440. { type: 'int2', value: 0, etype: 'smallint', evalue: '0' },
  441. { type: 'int4', value: 1, etype: 'integer', evalue: '1' },
  442. { type: 'int8', value: -1, etype: 'bigint', evalue: `'-1'::integer` },
  443. { type: 'float4', value: 0.1, etype: 'real', evalue: '0.1' },
  444. { type: 'float8', value: -0.1, etype: 'double precision', evalue: `'-0.1'::numeric` },
  445. { type: 'numeric', value: 1e2, etype: 'numeric', evalue: '100' },
  446. // json types
  447. {
  448. type: 'json',
  449. value: { a: 0, b: '1', c: true },
  450. etype: 'json',
  451. evalue: `'{\"a\": 0, \"b\": \"1\", \"c\": true}'::jsonb`,
  452. },
  453. // json array must be stringified, otherwise it will be converted to pg array literal
  454. { type: 'jsonb', value: JSON.stringify([null]), etype: 'jsonb', evalue: `'[null]'::jsonb` },
  455. // text types
  456. { type: 'text', value: `quote's`, etype: 'text', evalue: `'quote''s'::text` },
  457. { type: 'varchar', value: '\n', etype: 'character varying', evalue: `'\n'::character varying` },
  458. // datetime types
  459. {
  460. type: 'timestamp',
  461. value: `now() - INTERVAL '1 day'`,
  462. etype: 'timestamp without time zone',
  463. evalue: `(now() - '1 day'::interval)`,
  464. exp: true,
  465. },
  466. {
  467. type: 'timestamptz',
  468. value: 'NOW()',
  469. etype: 'timestamp with time zone',
  470. evalue: 'now()',
  471. exp: true,
  472. },
  473. { type: 'date', value: '2025-05-09', etype: 'date', evalue: `'2025-05-09'::date` },
  474. {
  475. type: 'time',
  476. value: '11:22:33',
  477. etype: 'time without time zone',
  478. evalue: `'11:22:33'::time without time zone`,
  479. },
  480. {
  481. type: 'timetz',
  482. value: '11:22:33+0800',
  483. etype: 'time with time zone',
  484. evalue: `'11:22:33+08'::time with time zone`,
  485. },
  486. // other types
  487. {
  488. type: 'uuid',
  489. value: 'gen_random_uuid()',
  490. etype: 'uuid',
  491. evalue: 'gen_random_uuid()',
  492. exp: true,
  493. },
  494. { type: 'bool', value: true, etype: 'boolean', evalue: 'true' },
  495. // https://www.postgresql.org/docs/current/datatype-binary.html#DATATYPE-BINARY-BYTEA-ESCAPE-FORMAT
  496. { type: 'bytea', value: `\\000`, etype: 'bytea', evalue: `'\\x00'::bytea` },
  497. ])('$type -> $value', async (c, { expect, task }) => {
  498. const id = { schema: 'public', table: 't', name: `c${task.id}` }
  499. // Create column with default value
  500. const { sql } = pgMeta.columns.create(
  501. c.exp
  502. ? {
  503. ...id,
  504. type: { name: c.type },
  505. default_value_format: 'expression',
  506. default_value: rawSql(String(c.value)),
  507. }
  508. : { ...id, type: { name: c.type }, default_value_format: 'literal', default_value: c.value }
  509. )
  510. await db.executeQuery(sql)
  511. // Retrieve and verify the created column
  512. const expected = pgMeta.columns.retrieve(id)
  513. const result = await db.executeQuery(expected.sql)
  514. const column = expected.zod.parse(result[0])
  515. expect(column).toStrictEqual({
  516. ...id,
  517. data_type: c.etype,
  518. default_value: c.evalue,
  519. format: c.type,
  520. format_schema: 'pg_catalog',
  521. id: expect.stringMatching(/^\d+\.\d+$/),
  522. ordinal_position: expect.any(Number),
  523. table_id: expect.any(Number),
  524. check: null,
  525. comment: null,
  526. enums: [],
  527. identity_generation: null,
  528. is_generated: false,
  529. is_identity: false,
  530. is_nullable: true,
  531. is_unique: false,
  532. is_updatable: true,
  533. })
  534. })
  535. })
  536. // https://github.com/supabase/supabase/issues/3553
  537. withTestDatabase('alter column to type with uppercase', async ({ executeQuery }) => {
  538. // Setup: Create table and type
  539. await executeQuery('CREATE TABLE t ()')
  540. await executeQuery('CREATE TYPE "T" AS ENUM ()')
  541. // Create and then update column
  542. const { sql: createSql } = await pgMeta.columns.create({
  543. schema: 'public',
  544. table: 't',
  545. name: 'c',
  546. type: { name: 'text' },
  547. is_unique: false,
  548. })
  549. await executeQuery(createSql)
  550. // Get column ID
  551. const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.columns.retrieve({
  552. schema: 'public',
  553. table: 't',
  554. name: 'c',
  555. })
  556. const column = retrieveZod.parse((await executeQuery(retrieveSql))[0])
  557. // Update column
  558. const { sql: updateSql } = await pgMeta.columns.update(column!, { type: { name: 'T' } })
  559. await executeQuery(updateSql)
  560. // Verify updated column
  561. const { sql: verifySQL, zod: verifyZod } = await pgMeta.columns.retrieve({
  562. schema: 'public',
  563. table: 't',
  564. name: 'c',
  565. })
  566. const updatedColumn = verifyZod.parse((await executeQuery(verifySQL))[0])
  567. expect(updatedColumn).toMatchInlineSnapshot(
  568. {
  569. id: expect.stringMatching(/^\d+\.1$/),
  570. table_id: expect.any(Number),
  571. },
  572. `
  573. {
  574. "check": null,
  575. "comment": null,
  576. "data_type": "USER-DEFINED",
  577. "default_value": null,
  578. "enums": [],
  579. "format": "T",
  580. "format_schema": "public",
  581. "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/,
  582. "identity_generation": null,
  583. "is_generated": false,
  584. "is_identity": false,
  585. "is_nullable": true,
  586. "is_unique": false,
  587. "is_updatable": true,
  588. "name": "c",
  589. "ordinal_position": 1,
  590. "schema": "public",
  591. "table": "t",
  592. "table_id": Any<Number>,
  593. }
  594. `
  595. )
  596. })
  597. withTestDatabase('enums are populated in enum array columns', async ({ executeQuery }) => {
  598. // Setup: Create type and table
  599. await executeQuery(`CREATE TYPE test_enum AS ENUM ('a')`)
  600. await executeQuery('CREATE TABLE t ()')
  601. // Create column
  602. const { sql: createSql } = pgMeta.columns.create({
  603. schema: 'public',
  604. table: 't',
  605. name: 'c',
  606. type: { name: 'test_enum', isArray: true },
  607. })
  608. await executeQuery(createSql)
  609. // Verify created column
  610. const { sql: verifySql, zod: verifyZod } = pgMeta.columns.retrieve({
  611. schema: 'public',
  612. table: 't',
  613. name: 'c',
  614. })
  615. const column = verifyZod.parse((await executeQuery(verifySql))[0])
  616. expect(column).toMatchInlineSnapshot(
  617. {
  618. id: expect.stringMatching(/^\d+\.1$/),
  619. table_id: expect.any(Number),
  620. },
  621. `
  622. {
  623. "check": null,
  624. "comment": null,
  625. "data_type": "ARRAY",
  626. "default_value": null,
  627. "enums": [
  628. "a",
  629. ],
  630. "format": "_test_enum",
  631. "format_schema": "public",
  632. "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/,
  633. "identity_generation": null,
  634. "is_generated": false,
  635. "is_identity": false,
  636. "is_nullable": true,
  637. "is_unique": false,
  638. "is_updatable": true,
  639. "name": "c",
  640. "ordinal_position": 1,
  641. "schema": "public",
  642. "table": "t",
  643. "table_id": Any<Number>,
  644. }
  645. `
  646. )
  647. })
  648. withTestDatabase('drop with cascade', async ({ executeQuery }) => {
  649. // Setup table with generated column
  650. await executeQuery(`
  651. create table public.t (
  652. id int8 primary key,
  653. t_id int8 generated always as (id) stored
  654. );
  655. `)
  656. // Retrieve column
  657. const { sql: retrieveSql, zod: retrieveZod } = pgMeta.columns.retrieve({
  658. schema: 'public',
  659. table: 't',
  660. name: 'id',
  661. })
  662. const column = retrieveZod.parse((await executeQuery(retrieveSql))[0])
  663. // Remove column with cascade
  664. const { sql: removeSql } = pgMeta.columns.remove(column!, { cascade: true })
  665. await executeQuery(removeSql)
  666. // Verify original column was removed
  667. const { sql: verifyIdSql, zod: verifyIdZod } = pgMeta.columns.retrieve({
  668. schema: 'public',
  669. table: 't',
  670. name: 'id',
  671. })
  672. const removedColumn = verifyIdZod.parse((await executeQuery(verifyIdSql))[0])
  673. expect(removedColumn).toBeUndefined()
  674. // Verify dependent column was also removed
  675. const { sql: verifyTIdSql, zod: verifyTIdZod } = pgMeta.columns.retrieve({
  676. schema: 'public',
  677. table: 't',
  678. name: 't_id',
  679. })
  680. const dependentColumn = verifyTIdZod.parse((await executeQuery(verifyTIdSql))[0])
  681. expect(dependentColumn).toBeUndefined()
  682. })
  683. withTestDatabase('column with multiple checks', async ({ executeQuery }) => {
  684. // Setup table with multiple check constraints
  685. await executeQuery('create table t(c int8 check (c != 0) check (c != -1))')
  686. // List columns
  687. const { sql, zod } = pgMeta.columns.list()
  688. const res = zod.parse(await executeQuery(sql))
  689. const columns = res
  690. .filter((c) => c.schema === 'public' && c.table === 't')
  691. .map(({ id, table_id, ...c }) => c)
  692. expect(columns).toMatchInlineSnapshot(`
  693. [
  694. {
  695. "check": "c <> 0",
  696. "comment": null,
  697. "data_type": "bigint",
  698. "default_value": null,
  699. "enums": [],
  700. "format": "int8",
  701. "format_schema": "pg_catalog",
  702. "identity_generation": null,
  703. "is_generated": false,
  704. "is_identity": false,
  705. "is_nullable": true,
  706. "is_unique": false,
  707. "is_updatable": true,
  708. "name": "c",
  709. "ordinal_position": 1,
  710. "schema": "public",
  711. "table": "t",
  712. },
  713. ]
  714. `)
  715. })
  716. withTestDatabase('column with multiple unique constraints', async ({ executeQuery }) => {
  717. // Setup table with multiple unique constraints
  718. await executeQuery(`create table t(c int8 unique); alter table t add unique (c);`)
  719. // List columns
  720. const { sql, zod } = await pgMeta.columns.list()
  721. const res = zod.parse(await executeQuery(sql))
  722. const columns = res
  723. .filter((c) => c.schema === 'public' && c.table === 't')
  724. .map(({ id, table_id, ...c }) => c)
  725. expect(columns).toMatchInlineSnapshot(`
  726. [
  727. {
  728. "check": null,
  729. "comment": null,
  730. "data_type": "bigint",
  731. "default_value": null,
  732. "enums": [],
  733. "format": "int8",
  734. "format_schema": "pg_catalog",
  735. "identity_generation": null,
  736. "is_generated": false,
  737. "is_identity": false,
  738. "is_nullable": true,
  739. "is_unique": true,
  740. "is_updatable": true,
  741. "name": "c",
  742. "ordinal_position": 1,
  743. "schema": "public",
  744. "table": "t",
  745. },
  746. ]
  747. `)
  748. })
  749. withTestDatabase('dropping column checks', async ({ executeQuery }) => {
  750. // Setup table with check constraint
  751. await executeQuery(`create table public.t(c int8 check (c != 0))`)
  752. // Retrieve column
  753. const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.columns.retrieve({
  754. schema: 'public',
  755. table: 't',
  756. name: 'c',
  757. })
  758. const column = retrieveZod.parse((await executeQuery(retrieveSql))[0])
  759. // Update column to remove check
  760. const { sql: updateSql } = await pgMeta.columns.update(column!, { check: null })
  761. await executeQuery(updateSql)
  762. // Verify updated column
  763. const { sql: verifySql, zod: verifyZod } = await pgMeta.columns.retrieve({
  764. schema: 'public',
  765. table: 't',
  766. name: 'c',
  767. })
  768. const updatedColumn = verifyZod.parse((await executeQuery(verifySql))[0])
  769. expect(updatedColumn!.check).toMatchInlineSnapshot(`null`)
  770. })
  771. withTestDatabase('column with fully-qualified type', async ({ executeQuery }) => {
  772. // Setup: Create table and type in separate schema
  773. await executeQuery(`
  774. create table public.t();
  775. create schema s;
  776. create type s.my_type as enum ();
  777. `)
  778. // Create column with fully-qualified type
  779. const { sql: createColumnSql } = await pgMeta.columns.create({
  780. schema: 'public',
  781. table: 't',
  782. name: 'c',
  783. type: { schema: 's', name: 'my_type' },
  784. })
  785. await executeQuery(createColumnSql)
  786. // Retrieve and verify the created column
  787. const { sql: retrieveSql, zod: retrieveZod } = await pgMeta.columns.retrieve({
  788. schema: 'public',
  789. table: 't',
  790. name: 'c',
  791. })
  792. const column = retrieveZod.parse((await executeQuery(retrieveSql))[0])
  793. expect(column).toMatchInlineSnapshot(
  794. {
  795. id: expect.stringMatching(/^\d+\.1$/),
  796. table_id: expect.any(Number),
  797. },
  798. `
  799. {
  800. "check": null,
  801. "comment": null,
  802. "data_type": "USER-DEFINED",
  803. "default_value": null,
  804. "enums": [],
  805. "format": "my_type",
  806. "format_schema": "s",
  807. "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/,
  808. "identity_generation": null,
  809. "is_generated": false,
  810. "is_identity": false,
  811. "is_nullable": true,
  812. "is_unique": false,
  813. "is_updatable": true,
  814. "name": "c",
  815. "ordinal_position": 1,
  816. "schema": "public",
  817. "table": "t",
  818. "table_id": Any<Number>,
  819. }
  820. `
  821. )
  822. })
  823. withTestDatabase('format_schema for built-in type', async ({ executeQuery }) => {
  824. await executeQuery(`create table t (c int4)`)
  825. const { sql, zod } = pgMeta.columns.retrieve({ schema: 'public', table: 't', name: 'c' })
  826. const column = zod.parse((await executeQuery(sql))[0])
  827. expect(column?.format).toBe('int4')
  828. expect(column?.format_schema).toBe('pg_catalog')
  829. })
  830. withTestDatabase('format_schema for public-schema enum', async ({ executeQuery }) => {
  831. await executeQuery(`create type public_enum as enum ('a'); create table t (c public_enum);`)
  832. const { sql, zod } = pgMeta.columns.retrieve({ schema: 'public', table: 't', name: 'c' })
  833. const column = zod.parse((await executeQuery(sql))[0])
  834. expect(column?.format).toBe('public_enum')
  835. expect(column?.format_schema).toBe('public')
  836. })
  837. withTestDatabase('format_schema for non-public-schema enum', async ({ executeQuery }) => {
  838. await executeQuery(`
  839. create schema s;
  840. create type s.my_enum as enum ('a');
  841. create table t (c s.my_enum);
  842. `)
  843. const { sql, zod } = pgMeta.columns.retrieve({ schema: 'public', table: 't', name: 'c' })
  844. const column = zod.parse((await executeQuery(sql))[0])
  845. expect(column?.format).toBe('my_enum')
  846. expect(column?.format_schema).toBe('s')
  847. })
  848. withTestDatabase('format_schema for array of non-public-schema enum', async ({ executeQuery }) => {
  849. await executeQuery(`
  850. create schema s;
  851. create type s.my_enum as enum ('a');
  852. create table t (c s.my_enum[]);
  853. `)
  854. const { sql, zod } = pgMeta.columns.retrieve({ schema: 'public', table: 't', name: 'c' })
  855. const column = zod.parse((await executeQuery(sql))[0])
  856. expect(column?.data_type).toBe('ARRAY')
  857. expect(column?.format).toBe('_my_enum')
  858. expect(column?.format_schema).toBe('s')
  859. })
  860. withTestDatabase(
  861. 'format_schema for domain over non-public-schema base type',
  862. async ({ executeQuery }) => {
  863. await executeQuery(`
  864. create schema s;
  865. create domain s.my_domain as int4;
  866. create table t (c s.my_domain);
  867. `)
  868. const { sql, zod } = pgMeta.columns.retrieve({ schema: 'public', table: 't', name: 'c' })
  869. const column = zod.parse((await executeQuery(sql))[0])
  870. // Domain's `format` resolves to the base type, so `format_schema` follows the base type.
  871. expect(column?.format).toBe('int4')
  872. expect(column?.format_schema).toBe('pg_catalog')
  873. }
  874. )