advanced-query.test.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. import { afterAll, describe, expect, test } from 'vitest'
  2. import { ident, joinSqlFragments, safeSql } from '../../src/pg-format'
  3. import { Query } from '../../src/query/Query'
  4. import { cleanupRoot, createTestDatabase } from '../db/utils'
  5. type TestDb = Awaited<ReturnType<typeof createTestDatabase>>
  6. async function validateSql(db: TestDb, sql: string): Promise<any> {
  7. try {
  8. const result = await db.executeQuery(sql)
  9. return result
  10. } catch (error) {
  11. throw new Error(`Invalid SQL generated: ${sql}\nError: ${error}`)
  12. }
  13. }
  14. const withTestDatabase = (name: string, fn: (db: TestDb) => Promise<void>) => {
  15. test(name, async () => {
  16. const db = await createTestDatabase()
  17. try {
  18. // Setup test tables with special characters, spaces, and quotes
  19. await db.executeQuery(`
  20. CREATE TABLE "public"."normal_table" (
  21. id SERIAL PRIMARY KEY,
  22. name TEXT
  23. );
  24. CREATE TABLE "public"."table with spaces" (
  25. id SERIAL PRIMARY KEY,
  26. "column with spaces" TEXT,
  27. "quoted""column" TEXT,
  28. "quoted'column" TEXT,
  29. "camelCaseColumn" TEXT,
  30. "special#$%^&Column" TEXT
  31. );
  32. CREATE TABLE "public"."quoted""table" (
  33. id SERIAL PRIMARY KEY,
  34. name TEXT
  35. );
  36. CREATE TABLE "public"."quoted'table" (
  37. id SERIAL PRIMARY KEY,
  38. name TEXT
  39. );
  40. CREATE TABLE "public"."camelCaseTable" (
  41. id SERIAL PRIMARY KEY,
  42. name TEXT
  43. );
  44. CREATE TABLE "public"."special#$%^&Table" (
  45. id SERIAL PRIMARY KEY,
  46. name TEXT
  47. );
  48. `)
  49. // Insert test data into each table
  50. await db.executeQuery(`
  51. -- Add data to normal_table
  52. INSERT INTO "public"."normal_table" (name)
  53. VALUES
  54. ('John Doe'),
  55. ('Jane Smith'),
  56. ('O''Reilly Books'),
  57. (NULL);
  58. -- Add data to table with spaces
  59. INSERT INTO "public"."table with spaces" (
  60. "column with spaces",
  61. "quoted""column",
  62. "quoted'column",
  63. "camelCaseColumn",
  64. "special#$%^&Column"
  65. )
  66. VALUES
  67. ('value with spaces', 'value with "quotes"', 'value with ''quotes''', 'camelCaseValue', 'special#$%^&Value'),
  68. ('another value', 'another "quoted" value', 'another ''quoted'' value', 'anotherCamelCase', 'another#$%^&');
  69. -- Add data to quoted"table
  70. INSERT INTO "public"."quoted""table" (name)
  71. VALUES
  72. ('quoted table row 1'),
  73. ('quoted table row 2');
  74. -- Add data to quoted'table
  75. INSERT INTO "public"."quoted'table" (name)
  76. VALUES
  77. ('single quoted table row 1'),
  78. ('single quoted table row 2');
  79. -- Add data to camelCaseTable
  80. INSERT INTO "public"."camelCaseTable" (name)
  81. VALUES
  82. ('camel case table row 1'),
  83. ('camel case table row 2');
  84. -- Add data to special#$%^&Table
  85. INSERT INTO "public"."special#$%^&Table" (name)
  86. VALUES
  87. ('special char table row 1'),
  88. ('special char table row 2');
  89. `)
  90. await fn(db)
  91. } finally {
  92. await db.cleanup()
  93. }
  94. })
  95. }
  96. describe('Advanced Query Tests', () => {
  97. afterAll(async () => {
  98. await cleanupRoot()
  99. })
  100. describe('Special Table and Column Names', () => {
  101. withTestDatabase('should handle tables with spaces', async (db) => {
  102. const query = new Query()
  103. const sql = query.from('table with spaces', 'public').select().toSql()
  104. expect(sql).toMatchInlineSnapshot(`"select * from public."table with spaces";"`)
  105. const result = await validateSql(db, sql)
  106. expect(result.length).toBe(2)
  107. expect(result[0]['column with spaces']).toBe('value with spaces')
  108. expect(result[1]['column with spaces']).toBe('another value')
  109. })
  110. withTestDatabase('should handle tables with double quotes', async (db) => {
  111. const query = new Query()
  112. const sql = query.from('quoted"table', 'public').select().toSql()
  113. expect(sql).toMatchInlineSnapshot(`"select * from public."quoted""table";"`)
  114. const result = await validateSql(db, sql)
  115. expect(result.length).toBe(2)
  116. expect(result[0].name).toBe('quoted table row 1')
  117. expect(result[1].name).toBe('quoted table row 2')
  118. })
  119. withTestDatabase('should handle tables with single quotes', async (db) => {
  120. const query = new Query()
  121. const sql = query.from("quoted'table", 'public').select().toSql()
  122. expect(sql).toMatchInlineSnapshot(`"select * from public."quoted'table";"`)
  123. const result = await validateSql(db, sql)
  124. expect(result.length).toBe(2)
  125. expect(result[0].name).toBe('single quoted table row 1')
  126. expect(result[1].name).toBe('single quoted table row 2')
  127. })
  128. withTestDatabase('should handle camelCase table names', async (db) => {
  129. const query = new Query()
  130. const sql = query.from('camelCaseTable', 'public').select().toSql()
  131. expect(sql).toMatchInlineSnapshot(`"select * from public."camelCaseTable";"`)
  132. const result = await validateSql(db, sql)
  133. expect(result.length).toBe(2)
  134. expect(result[0].name).toBe('camel case table row 1')
  135. expect(result[1].name).toBe('camel case table row 2')
  136. })
  137. withTestDatabase('should handle tables with special characters', async (db) => {
  138. const query = new Query()
  139. const sql = query.from('special#$%^&Table', 'public').select().toSql()
  140. expect(sql).toMatchInlineSnapshot(`"select * from public."special#$%^&Table";"`)
  141. const result = await validateSql(db, sql)
  142. expect(result.length).toBe(2)
  143. expect(result[0].name).toBe('special char table row 1')
  144. expect(result[1].name).toBe('special char table row 2')
  145. })
  146. withTestDatabase('should handle columns with spaces', async (db) => {
  147. const query = new Query()
  148. const sql = query
  149. .from('table with spaces', 'public')
  150. .select(safeSql`"column with spaces"`)
  151. .toSql()
  152. expect(sql).toMatchInlineSnapshot(
  153. `"select "column with spaces" from public."table with spaces";"`
  154. )
  155. const result = await validateSql(db, sql)
  156. expect(result.length).toBe(2)
  157. expect(result[0]['column with spaces']).toBe('value with spaces')
  158. expect(result[1]['column with spaces']).toBe('another value')
  159. })
  160. withTestDatabase('should handle columns with double quotes', async (db) => {
  161. const query = new Query()
  162. const sql = query
  163. .from('table with spaces', 'public')
  164. .select(safeSql`"quoted""column"`)
  165. .toSql()
  166. expect(sql).toMatchInlineSnapshot(
  167. `"select "quoted""column" from public."table with spaces";"`
  168. )
  169. const result = await validateSql(db, sql)
  170. expect(result.length).toBe(2)
  171. expect(result[0]['quoted"column']).toBe('value with "quotes"')
  172. expect(result[1]['quoted"column']).toBe('another "quoted" value')
  173. })
  174. withTestDatabase('should handle columns with single quotes', async (db) => {
  175. const query = new Query()
  176. const sql = query
  177. .from('table with spaces', 'public')
  178. .select(safeSql`"quoted'column"`)
  179. .toSql()
  180. expect(sql).toMatchInlineSnapshot(`"select "quoted'column" from public."table with spaces";"`)
  181. const result = await validateSql(db, sql)
  182. expect(result.length).toBe(2)
  183. expect(result[0]["quoted'column"]).toBe("value with 'quotes'")
  184. expect(result[1]["quoted'column"]).toBe("another 'quoted' value")
  185. })
  186. withTestDatabase('should handle camelCase column names', async (db) => {
  187. const query = new Query()
  188. const sql = query
  189. .from('table with spaces', 'public')
  190. .select(safeSql`"camelCaseColumn"`)
  191. .toSql()
  192. expect(sql).toMatchInlineSnapshot(
  193. `"select "camelCaseColumn" from public."table with spaces";"`
  194. )
  195. const result = await validateSql(db, sql)
  196. expect(result.length).toBe(2)
  197. expect(result[0].camelCaseColumn).toBe('camelCaseValue')
  198. expect(result[1].camelCaseColumn).toBe('anotherCamelCase')
  199. })
  200. withTestDatabase('should handle columns with special characters', async (db) => {
  201. const query = new Query()
  202. const sql = query
  203. .from('table with spaces', 'public')
  204. .select(safeSql`"special#$%^&Column"`)
  205. .toSql()
  206. expect(sql).toMatchInlineSnapshot(
  207. `"select "special#$%^&Column" from public."table with spaces";"`
  208. )
  209. const result = await validateSql(db, sql)
  210. expect(result.length).toBe(2)
  211. expect(result[0]['special#$%^&Column']).toBe('special#$%^&Value')
  212. expect(result[1]['special#$%^&Column']).toBe('another#$%^&')
  213. })
  214. })
  215. describe('Complex Queries with Special Names', () => {
  216. withTestDatabase('should handle filtering on columns with spaces', async (db) => {
  217. // First ensure the table exists with the right column
  218. await db.executeQuery(`
  219. DROP TABLE IF EXISTS "public"."table with spaces";
  220. CREATE TABLE "public"."table with spaces" (
  221. id SERIAL PRIMARY KEY,
  222. "column with spaces" TEXT
  223. );
  224. -- Insert test data
  225. INSERT INTO "public"."table with spaces" ("column with spaces")
  226. VALUES ('test value'), ('other value');
  227. `)
  228. const query = new Query()
  229. // Specify the column name without extra quotes in the filter
  230. // The Query class handles the proper quoting
  231. const sql = query
  232. .from('table with spaces', 'public')
  233. .select()
  234. .filter('column with spaces', '=', 'test value')
  235. .toSql()
  236. expect(sql).toMatchInlineSnapshot(
  237. `"select * from public."table with spaces" where "column with spaces" = 'test value';"`
  238. )
  239. // Validate the generated SQL directly against the database
  240. const result = await validateSql(db, sql)
  241. expect(result.length).toBe(1)
  242. expect(result[0]['column with spaces']).toBe('test value')
  243. })
  244. withTestDatabase('should handle filtering with values containing quotes', async (db) => {
  245. await db.executeQuery(`
  246. INSERT INTO "public"."normal_table" (name)
  247. VALUES ('O''Reilly');
  248. `)
  249. const query = new Query()
  250. const sql = query
  251. .from('normal_table', 'public')
  252. .select()
  253. .filter('name', '=', "O'Reilly")
  254. .toSql()
  255. expect(sql).toMatchInlineSnapshot(
  256. `"select * from public.normal_table where name = 'O''Reilly';"`
  257. )
  258. const result = await validateSql(db, sql)
  259. expect(result.length).toBe(1)
  260. expect(result[0].name).toBe("O'Reilly")
  261. })
  262. withTestDatabase('should handle updating with values containing quotes', async (db) => {
  263. const query = new Query()
  264. const sql = query
  265. .from('normal_table', 'public')
  266. .update({ name: "John O'Reilly" }, { returning: true })
  267. .filter('id', '=', 1)
  268. .toSql()
  269. expect(sql).toMatchInlineSnapshot(
  270. `"update public.normal_table set (name) = (select name from json_populate_record(null::public.normal_table, '{"name":"John O''Reilly"}')) where id = 1 returning *;"`
  271. )
  272. await validateSql(db, sql)
  273. })
  274. withTestDatabase('should handle inserting with values containing quotes', async (db) => {
  275. const query = new Query()
  276. const sql = query
  277. .from('normal_table', 'public')
  278. .insert([{ name: "John O'Reilly" }], { returning: true })
  279. .toSql()
  280. expect(sql).toMatchInlineSnapshot(
  281. `"insert into public.normal_table (name) select name from jsonb_populate_recordset(null::public.normal_table, '[{"name":"John O''Reilly"}]') returning *;"`
  282. )
  283. await validateSql(db, sql)
  284. })
  285. })
  286. describe('Advanced SQL Generation and Validation', () => {
  287. withTestDatabase(
  288. 'should generate valid select with multiple filters and sorting',
  289. async (db) => {
  290. await db.executeQuery(`
  291. DELETE FROM "public"."normal_table";
  292. INSERT INTO "public"."normal_table" (id, name)
  293. VALUES
  294. (11, 'John Smith'),
  295. (12, 'John Doe'),
  296. (13, 'Jane Smith'),
  297. (14, 'Someone Else');
  298. `)
  299. const query = new Query()
  300. const sql = query
  301. .from('normal_table', 'public')
  302. .select(safeSql`id, name`)
  303. .filter('id', '>', 10)
  304. .filter('name', '~~', '%John%')
  305. .order('normal_table', 'name', true, false)
  306. .range(0, 9)
  307. .toSql()
  308. expect(sql).toMatchInlineSnapshot(
  309. `"select id, name from public.normal_table where id > 10 and name::text ~~ '%John%' order by normal_table.name asc nulls last limit 10 offset 0;"`
  310. )
  311. const result = await validateSql(db, sql)
  312. expect(result.length).toBe(2)
  313. expect(result[0].name).toBe('John Doe') // Alphabetically first
  314. expect(result[1].name).toBe('John Smith')
  315. expect(result.every((row: any) => row.id > 10)).toBe(true)
  316. }
  317. )
  318. withTestDatabase('should generate valid insert with returning clause', async (db) => {
  319. const query = new Query()
  320. const sql = query
  321. .from('normal_table', 'public')
  322. .insert([{ name: 'John Doe' }], { returning: true })
  323. .toSql()
  324. expect(sql).toMatchInlineSnapshot(
  325. `"insert into public.normal_table (name) select name from jsonb_populate_recordset(null::public.normal_table, '[{"name":"John Doe"}]') returning *;"`
  326. )
  327. const result = await validateSql(db, sql)
  328. expect(result.length).toBe(1)
  329. expect(result[0].name).toBe('John Doe')
  330. })
  331. withTestDatabase('should generate valid update with filtering', async (db) => {
  332. await db.executeQuery(`
  333. -- Clear and insert test data
  334. DELETE FROM "public"."normal_table";
  335. INSERT INTO "public"."normal_table" (id, name)
  336. VALUES (1, 'Original Name') ON CONFLICT (id) DO UPDATE SET name = 'Original Name';
  337. `)
  338. const query = new Query()
  339. const sql = query
  340. .from('normal_table', 'public')
  341. .update({ name: 'Updated Name' }, { returning: true })
  342. .filter('id', '=', 1)
  343. .toSql()
  344. expect(sql).toMatchInlineSnapshot(
  345. `"update public.normal_table set (name) = (select name from json_populate_record(null::public.normal_table, '{"name":"Updated Name"}')) where id = 1 returning *;"`
  346. )
  347. const result = await validateSql(db, sql)
  348. expect(result.length).toBe(1)
  349. expect(result[0].id).toBe(1)
  350. expect(result[0].name).toBe('Updated Name')
  351. // Verify the update was actually persisted
  352. const verifyResult = await db.executeQuery('SELECT * FROM public.normal_table WHERE id = 1')
  353. expect(verifyResult[0].name).toBe('Updated Name')
  354. })
  355. withTestDatabase('should generate valid delete with filtering', async (db) => {
  356. await db.executeQuery(`
  357. -- Clear and insert test data
  358. DELETE FROM "public"."normal_table";
  359. INSERT INTO "public"."normal_table" (id, name)
  360. VALUES (1, 'To Be Deleted') ON CONFLICT (id) DO UPDATE SET name = 'To Be Deleted';
  361. `)
  362. const query = new Query()
  363. const sql = query
  364. .from('normal_table', 'public')
  365. .delete({ returning: true })
  366. .filter('id', '=', 1)
  367. .toSql()
  368. expect(sql).toMatchInlineSnapshot(
  369. '"delete from public.normal_table where id = 1 returning *;"'
  370. )
  371. const result = await validateSql(db, sql)
  372. expect(result.length).toBe(1)
  373. expect(result[0].id).toBe(1)
  374. expect(result[0].name).toBe('To Be Deleted')
  375. // Verify the row was actually deleted
  376. const verifyResult = await db.executeQuery('SELECT * FROM public.normal_table WHERE id = 1')
  377. expect(verifyResult.length).toBe(0)
  378. })
  379. withTestDatabase('should generate valid count with filtering', async (db) => {
  380. await db.executeQuery(`
  381. -- Clear and insert test data
  382. DELETE FROM "public"."normal_table";
  383. INSERT INTO "public"."normal_table" (name)
  384. VALUES ('John Smith'), ('John Doe'), ('Jane Doe');
  385. `)
  386. const query = new Query()
  387. const sql = query
  388. .from('normal_table', 'public')
  389. .count()
  390. .filter('name', '~~', '%John%')
  391. .toSql()
  392. expect(sql).toMatchInlineSnapshot(
  393. '"select count(*) from public.normal_table where name::text ~~ \'%John%\';"'
  394. )
  395. const result = await validateSql(db, sql)
  396. expect(result[0].count).toBe(2) // PostgreSQL returns count as string
  397. })
  398. withTestDatabase('should generate valid truncate query', async (db) => {
  399. await db.executeQuery(`
  400. INSERT INTO "public"."normal_table" (name)
  401. VALUES ('Test Row 1'), ('Test Row 2');
  402. `)
  403. // Verify data exists
  404. const beforeCount = await db.executeQuery(`SELECT COUNT(*) FROM "public"."normal_table"`)
  405. expect(parseInt(beforeCount[0].count)).toBeGreaterThan(0)
  406. const query = new Query()
  407. const sql = query.from('normal_table', 'public').truncate().toSql()
  408. expect(sql).toMatchInlineSnapshot('"truncate public.normal_table;"')
  409. await validateSql(db, sql)
  410. // Verify truncate worked
  411. const afterCount = await db.executeQuery(`SELECT COUNT(*) FROM "public"."normal_table"`)
  412. expect(parseInt(afterCount[0].count)).toBe(0)
  413. })
  414. })
  415. describe('Corner Cases and Error Handling', () => {
  416. withTestDatabase('should throw error for delete without filters', async () => {
  417. const query = new Query()
  418. const action = query.from('normal_table', 'public').delete({ returning: true })
  419. expect(() => action.toSql()).toThrow(/no filters/)
  420. })
  421. withTestDatabase('should throw error for update without filters', async () => {
  422. const query = new Query()
  423. const action = query.from('normal_table', 'public').update({ name: 'Updated Name' })
  424. expect(() => action.toSql()).toThrow(/no filters/)
  425. })
  426. withTestDatabase('should throw error for insert without values', async () => {
  427. const query = new Query()
  428. // We're passing an empty array to test the runtime error
  429. const action = query.from('normal_table', 'public').insert([] as any, { returning: true })
  430. expect(() => action.toSql()).toThrow(/no value to insert/)
  431. })
  432. withTestDatabase('should handle special characters in values', async (db) => {
  433. const query = new Query()
  434. const sql = query
  435. .from('normal_table', 'public')
  436. .select()
  437. .filter('name', '=', 'Special $ ^ & * ( ) _ + { } | : < > ? characters')
  438. .toSql()
  439. expect(sql).toMatchInlineSnapshot(
  440. `"select * from public.normal_table where name = 'Special $ ^ & * ( ) _ + { } | : < > ? characters';"`
  441. )
  442. await validateSql(db, sql)
  443. })
  444. })
  445. describe('Advanced Filtering', () => {
  446. withTestDatabase('should handle "in" operator with array values', async (db) => {
  447. await db.executeQuery(`
  448. DELETE FROM "public"."normal_table";
  449. INSERT INTO "public"."normal_table" (id, name)
  450. VALUES
  451. (1, 'Row 1'),
  452. (2, 'Row 2'),
  453. (3, 'Row 3'),
  454. (4, 'Row 4');
  455. `)
  456. const query = new Query()
  457. const sql = query
  458. .from('normal_table', 'public')
  459. .select()
  460. .filter('id', 'in', [1, 2, 3])
  461. .toSql()
  462. expect(sql).toMatchInlineSnapshot(`"select * from public.normal_table where id in (1,2,3);"`)
  463. const result = await validateSql(db, sql)
  464. expect(result.length).toBe(3)
  465. expect(result.map((row: any) => row.id).sort()).toEqual([1, 2, 3])
  466. })
  467. withTestDatabase('should handle "is" operator with null value', async (db) => {
  468. await db.executeQuery(`
  469. DELETE FROM "public"."normal_table";
  470. INSERT INTO "public"."normal_table" (id, name)
  471. VALUES
  472. (1, 'Not Null'),
  473. (2, NULL);
  474. `)
  475. const query = new Query()
  476. const sql = query.from('normal_table', 'public').select().filter('name', 'is', 'null').toSql()
  477. expect(sql).toMatchInlineSnapshot(`"select * from public.normal_table where name is null;"`)
  478. const result = await validateSql(db, sql)
  479. expect(result.length).toBe(1)
  480. expect(result[0].id).toBe(2)
  481. expect(result[0].name).toBeNull()
  482. })
  483. withTestDatabase('should handle "is" operator with not null value', async (db) => {
  484. await db.executeQuery(`
  485. DELETE FROM "public"."normal_table";
  486. INSERT INTO "public"."normal_table" (id, name)
  487. VALUES
  488. (1, 'Not Null'),
  489. (2, NULL);
  490. `)
  491. const query = new Query()
  492. const sql = query
  493. .from('normal_table', 'public')
  494. .select()
  495. .filter('name', 'is', 'not null')
  496. .toSql()
  497. expect(sql).toMatchInlineSnapshot(
  498. `"select * from public.normal_table where name is not null;"`
  499. )
  500. const result = await validateSql(db, sql)
  501. expect(result.length).toBe(1)
  502. expect(result[0].id).toBe(1)
  503. expect(result[0].name).toBe('Not Null')
  504. })
  505. })
  506. })