table-row-query.test.ts 58 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850
  1. import { afterAll, beforeAll, describe, expect, test } from 'vitest'
  2. import pgMeta from '../../src/index'
  3. import { Filter, Sort } from '../../src/query'
  4. import { getDefaultOrderByColumns, getTableRowsSql } from '../../src/query/table-row-query'
  5. import { cleanupRoot, createTestDatabase } from '../db/utils'
  6. beforeAll(async () => {
  7. // Any global setup if needed
  8. })
  9. afterAll(async () => {
  10. await cleanupRoot()
  11. })
  12. type TestDb = Awaited<ReturnType<typeof createTestDatabase>>
  13. const withTestDatabase = (name: string, fn: (db: TestDb) => Promise<void>) => {
  14. test(name, async () => {
  15. const db = await createTestDatabase()
  16. try {
  17. await fn(db)
  18. } finally {
  19. await db.cleanup()
  20. }
  21. })
  22. }
  23. describe('Table Row Query', () => {
  24. describe('getDefaultOrderByColumns', () => {
  25. test('should return empty array when no primary keys and no columns exist', () => {
  26. const result = getDefaultOrderByColumns({
  27. primary_keys: [],
  28. columns: [],
  29. })
  30. expect(result).toEqual([])
  31. })
  32. test('should exclude specified columns when determining default sort', () => {
  33. const table = {
  34. primary_keys: [{ name: 'id' }],
  35. columns: [
  36. {
  37. name: 'id',
  38. data_type: 'integer',
  39. format: 'int4',
  40. ordinal_position: 1,
  41. },
  42. {
  43. name: 'name',
  44. data_type: 'text',
  45. format: 'text',
  46. ordinal_position: 2,
  47. },
  48. ],
  49. } as any
  50. const result = getDefaultOrderByColumns(table, { excludedColumns: ['id'] })
  51. expect(result).toEqual(['name'])
  52. })
  53. })
  54. describe('getTableRowsSql', () => {
  55. withTestDatabase('should handle array of enums correctly', async (db) => {
  56. // Create an enum type and a table with an array of that enum
  57. await db.executeQuery(`
  58. -- Create enum type
  59. CREATE TYPE status_type AS ENUM ('pending', 'active', 'completed', 'canceled');
  60. -- Create table with array of enums
  61. CREATE TABLE test_enum_array (
  62. id SERIAL PRIMARY KEY,
  63. name TEXT,
  64. status status_type, -- Regular enum column
  65. history status_type[] -- Array of enums
  66. );
  67. -- Insert test data with various enum array values
  68. INSERT INTO test_enum_array (name, status, history) VALUES
  69. ('Item 1', 'active', ARRAY['pending', 'active']::status_type[]),
  70. ('Item 2', 'completed', ARRAY['pending', 'active', 'completed']::status_type[]),
  71. ('Item 3', 'canceled', ARRAY['active', 'canceled']::status_type[]),
  72. ('Item 4', 'pending', NULL);
  73. `)
  74. // Get table metadata
  75. const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
  76. const tables = tablesZod.parse(await db.executeQuery(tablesSql))
  77. const testTable = tables.find((table) => table.name === 'test_enum_array')
  78. expect(testTable).toBeDefined()
  79. // Generate SQL
  80. const sql = getTableRowsSql({
  81. table: testTable!,
  82. page: 1,
  83. limit: 10,
  84. })
  85. // Verify SQL generation with snapshot
  86. expect(sql).toMatchInlineSnapshot(`
  87. "with _base_query as (select * from public.test_enum_array order by test_enum_array.id asc nulls last limit 10 offset 0)
  88. select id,case
  89. when octet_length(name::text) > 10240
  90. then left(name::text, 10240) || '...'
  91. else name::text
  92. end as name,status,
  93. case
  94. when octet_length(history::text) > 10240
  95. then
  96. case
  97. when array_ndims(history) = 1
  98. then
  99. (select array_cat(history[1:50]::text[], array['...']::text[]))::text[]
  100. else
  101. history[1:50]::text[]
  102. end
  103. else history::text[]
  104. end
  105. from _base_query;"
  106. `)
  107. // Execute the generated SQL and verify the results
  108. const queryResult = await db.executeQuery(sql)
  109. expect(queryResult).toMatchInlineSnapshot(`
  110. [
  111. {
  112. "history": [
  113. "pending",
  114. "active",
  115. ],
  116. "id": 1,
  117. "name": "Item 1",
  118. "status": "active",
  119. },
  120. {
  121. "history": [
  122. "pending",
  123. "active",
  124. "completed",
  125. ],
  126. "id": 2,
  127. "name": "Item 2",
  128. "status": "completed",
  129. },
  130. {
  131. "history": [
  132. "active",
  133. "canceled",
  134. ],
  135. "id": 3,
  136. "name": "Item 3",
  137. "status": "canceled",
  138. },
  139. {
  140. "history": null,
  141. "id": 4,
  142. "name": "Item 4",
  143. "status": "pending",
  144. },
  145. ]
  146. `)
  147. // Test filtering on enum array values
  148. const filtersStatusSQL = getTableRowsSql({
  149. table: testTable!,
  150. filters: [
  151. { column: 'status', operator: '=', value: `active` }, // Contains 'active' using text pattern matching
  152. ],
  153. page: 1,
  154. limit: 10,
  155. })
  156. expect(filtersStatusSQL).toMatchInlineSnapshot(`
  157. "with _base_query as (select * from public.test_enum_array where status = 'active' order by test_enum_array.id asc nulls last limit 10 offset 0)
  158. select id,case
  159. when octet_length(name::text) > 10240
  160. then left(name::text, 10240) || '...'
  161. else name::text
  162. end as name,status,
  163. case
  164. when octet_length(history::text) > 10240
  165. then
  166. case
  167. when array_ndims(history) = 1
  168. then
  169. (select array_cat(history[1:50]::text[], array['...']::text[]))::text[]
  170. else
  171. history[1:50]::text[]
  172. end
  173. else history::text[]
  174. end
  175. from _base_query;"
  176. `)
  177. // Execute the filtered query
  178. const filtersStatusResult = await db.executeQuery(filtersStatusSQL)
  179. expect(filtersStatusResult).toMatchInlineSnapshot(`
  180. [
  181. {
  182. "history": [
  183. "pending",
  184. "active",
  185. ],
  186. "id": 1,
  187. "name": "Item 1",
  188. "status": "active",
  189. },
  190. ]
  191. `)
  192. const filtersHistorySQL = getTableRowsSql({
  193. table: testTable!,
  194. filters: [
  195. { column: 'history', operator: '=', value: `ARRAY['active']::status_type[]` }, // Contains 'active' using text pattern matching
  196. ],
  197. page: 1,
  198. limit: 10,
  199. })
  200. expect(filtersHistorySQL).toMatchInlineSnapshot(`
  201. "with _base_query as (select * from public.test_enum_array where history = ARRAY['active']::status_type[] order by test_enum_array.id asc nulls last limit 10 offset 0)
  202. select id,case
  203. when octet_length(name::text) > 10240
  204. then left(name::text, 10240) || '...'
  205. else name::text
  206. end as name,status,
  207. case
  208. when octet_length(history::text) > 10240
  209. then
  210. case
  211. when array_ndims(history) = 1
  212. then
  213. (select array_cat(history[1:50]::text[], array['...']::text[]))::text[]
  214. else
  215. history[1:50]::text[]
  216. end
  217. else history::text[]
  218. end
  219. from _base_query;"
  220. `)
  221. const filtersHistoryResult = await db.executeQuery(filtersHistorySQL)
  222. expect(filtersHistoryResult).toMatchInlineSnapshot(`[]`)
  223. })
  224. withTestDatabase('should handle array columns correctly', async (db) => {
  225. await db.executeQuery(`
  226. CREATE TABLE test_array_table (
  227. id SERIAL PRIMARY KEY,
  228. name TEXT,
  229. tags TEXT[] -- Array of text
  230. );
  231. -- Insert test data with array values
  232. INSERT INTO test_array_table (name, tags) VALUES
  233. ('Item 1', ARRAY['tag1', 'tag2']),
  234. ('Item 2', ARRAY['tag3']),
  235. ('Item 3', ARRAY['tag1', 'tag4']);
  236. `)
  237. // Get table metadata
  238. const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
  239. const tables = tablesZod.parse(await db.executeQuery(tablesSql))
  240. const testTable = tables.find((table) => table.name === 'test_array_table')
  241. expect(testTable).toBeDefined()
  242. // Generate SQL
  243. const sql = getTableRowsSql({
  244. table: testTable!,
  245. page: 1,
  246. limit: 10,
  247. })
  248. expect(sql).toMatchInlineSnapshot(`
  249. "with _base_query as (select * from public.test_array_table order by test_array_table.id asc nulls last limit 10 offset 0)
  250. select id,case
  251. when octet_length(name::text) > 10240
  252. then left(name::text, 10240) || '...'
  253. else name::text
  254. end as name,
  255. case
  256. when octet_length(tags::text) > 10240
  257. then
  258. case
  259. when array_ndims(tags) = 1
  260. then
  261. (select array_cat(tags[1:50]::text[], array['...']::text[]))::text[]
  262. else
  263. tags[1:50]::text[]
  264. end
  265. else tags::text[]
  266. end
  267. from _base_query;"
  268. `)
  269. const queryResult = await db.executeQuery(sql)
  270. expect(queryResult.length).toBe(3)
  271. expect(queryResult).toMatchInlineSnapshot(`
  272. [
  273. {
  274. "id": 1,
  275. "name": "Item 1",
  276. "tags": [
  277. "tag1",
  278. "tag2",
  279. ],
  280. },
  281. {
  282. "id": 2,
  283. "name": "Item 2",
  284. "tags": [
  285. "tag3",
  286. ],
  287. },
  288. {
  289. "id": 3,
  290. "name": "Item 3",
  291. "tags": [
  292. "tag1",
  293. "tag4",
  294. ],
  295. },
  296. ]
  297. `)
  298. })
  299. withTestDatabase('should generate basic SELECT SQL for a table', async (db) => {
  300. // Create test table and insert data
  301. await db.executeQuery(`
  302. CREATE TABLE test_sql_gen (
  303. id SERIAL PRIMARY KEY,
  304. name TEXT,
  305. description TEXT,
  306. created_at TIMESTAMP DEFAULT NOW()
  307. );
  308. -- Insert test data
  309. INSERT INTO test_sql_gen (name, description) VALUES
  310. ('Row 1', 'Description 1'),
  311. ('Row 2', 'Description 2'),
  312. ('Row 3', 'Description 3');
  313. `)
  314. // Get table metadata
  315. const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
  316. const tables = tablesZod.parse(await db.executeQuery(tablesSql))
  317. const testTable = tables.find((table) => table.name === 'test_sql_gen')
  318. expect(testTable).toBeDefined()
  319. // Generate SQL
  320. const sql = getTableRowsSql({
  321. table: testTable!,
  322. page: 1,
  323. limit: 10,
  324. })
  325. // Verify SQL generation with snapshot
  326. expect(sql).toMatchInlineSnapshot(
  327. `
  328. "with _base_query as (select * from public.test_sql_gen order by test_sql_gen.id asc nulls last limit 10 offset 0)
  329. select id,case
  330. when octet_length(name::text) > 10240
  331. then left(name::text, 10240) || '...'
  332. else name::text
  333. end as name,case
  334. when octet_length(description::text) > 10240
  335. then left(description::text, 10240) || '...'
  336. else description::text
  337. end as description,created_at from _base_query;"
  338. `
  339. )
  340. // E2E Test: Execute the SQL and verify results
  341. const queryResult = await db.executeQuery(sql)
  342. expect(queryResult.length).toBe(3)
  343. expect(queryResult.map((row: any) => row.name)).toEqual(['Row 1', 'Row 2', 'Row 3'])
  344. expect(queryResult.map((row: any) => row.description)).toEqual([
  345. 'Description 1',
  346. 'Description 2',
  347. 'Description 3',
  348. ])
  349. })
  350. withTestDatabase(
  351. 'should truncate large arrays to maxArraySize elements if their size is > maxCharacters',
  352. async (db) => {
  353. // Create test table with array column
  354. await db.executeQuery(`
  355. CREATE TABLE test_large_array_table (
  356. id SERIAL PRIMARY KEY,
  357. name TEXT,
  358. large_array TEXT[] -- Will hold a very large array
  359. );
  360. -- Insert test data with a large array (>10KB)
  361. -- Create an array with 1000 elements to ensure it exceeds 10KB
  362. INSERT INTO test_large_array_table (name, large_array) VALUES
  363. ('Large Array Item', (SELECT array_agg('element_' || i) FROM generate_series(1, 1000) i)),
  364. ('Large Array Small items', (SELECT array_agg('' || i) FROM generate_series(1, 100) i)),
  365. ('Normal Array Item', ARRAY['tag1', 'tag2', 'tag3']);
  366. `)
  367. // Get table metadata
  368. const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
  369. const tables = tablesZod.parse(await db.executeQuery(tablesSql))
  370. const testTable = tables.find((table) => table.name === 'test_large_array_table')
  371. expect(testTable).toBeDefined()
  372. // Generate SQL with lower maxCharacters and maxArraySize limits
  373. const sql = getTableRowsSql({
  374. table: testTable!,
  375. page: 1,
  376. limit: 10,
  377. maxCharacters: 2048,
  378. maxArraySize: 10,
  379. })
  380. // Verify the SQL contains the array truncation logic
  381. expect(sql).toMatchInlineSnapshot(`
  382. "with _base_query as (select * from public.test_large_array_table order by test_large_array_table.id asc nulls last limit 10 offset 0)
  383. select id,case
  384. when octet_length(name::text) > 2048
  385. then left(name::text, 2048) || '...'
  386. else name::text
  387. end as name,
  388. case
  389. when octet_length(large_array::text) > 2048
  390. then
  391. case
  392. when array_ndims(large_array) = 1
  393. then
  394. (select array_cat(large_array[1:10]::text[], array['...']::text[]))::text[]
  395. else
  396. large_array[1:10]::text[]
  397. end
  398. else large_array::text[]
  399. end
  400. from _base_query;"
  401. `)
  402. // Execute the SQL and verify results
  403. const queryResult = await db.executeQuery(sql)
  404. expect(queryResult).toMatchInlineSnapshot(`
  405. [
  406. {
  407. "id": 1,
  408. "large_array": [
  409. "element_1",
  410. "element_2",
  411. "element_3",
  412. "element_4",
  413. "element_5",
  414. "element_6",
  415. "element_7",
  416. "element_8",
  417. "element_9",
  418. "element_10",
  419. "...",
  420. ],
  421. "name": "Large Array Item",
  422. },
  423. {
  424. "id": 2,
  425. "large_array": [
  426. "1",
  427. "2",
  428. "3",
  429. "4",
  430. "5",
  431. "6",
  432. "7",
  433. "8",
  434. "9",
  435. "10",
  436. "11",
  437. "12",
  438. "13",
  439. "14",
  440. "15",
  441. "16",
  442. "17",
  443. "18",
  444. "19",
  445. "20",
  446. "21",
  447. "22",
  448. "23",
  449. "24",
  450. "25",
  451. "26",
  452. "27",
  453. "28",
  454. "29",
  455. "30",
  456. "31",
  457. "32",
  458. "33",
  459. "34",
  460. "35",
  461. "36",
  462. "37",
  463. "38",
  464. "39",
  465. "40",
  466. "41",
  467. "42",
  468. "43",
  469. "44",
  470. "45",
  471. "46",
  472. "47",
  473. "48",
  474. "49",
  475. "50",
  476. "51",
  477. "52",
  478. "53",
  479. "54",
  480. "55",
  481. "56",
  482. "57",
  483. "58",
  484. "59",
  485. "60",
  486. "61",
  487. "62",
  488. "63",
  489. "64",
  490. "65",
  491. "66",
  492. "67",
  493. "68",
  494. "69",
  495. "70",
  496. "71",
  497. "72",
  498. "73",
  499. "74",
  500. "75",
  501. "76",
  502. "77",
  503. "78",
  504. "79",
  505. "80",
  506. "81",
  507. "82",
  508. "83",
  509. "84",
  510. "85",
  511. "86",
  512. "87",
  513. "88",
  514. "89",
  515. "90",
  516. "91",
  517. "92",
  518. "93",
  519. "94",
  520. "95",
  521. "96",
  522. "97",
  523. "98",
  524. "99",
  525. "100",
  526. ],
  527. "name": "Large Array Small items",
  528. },
  529. {
  530. "id": 3,
  531. "large_array": [
  532. "tag1",
  533. "tag2",
  534. "tag3",
  535. ],
  536. "name": "Normal Array Item",
  537. },
  538. ]
  539. `)
  540. }
  541. )
  542. withTestDatabase(
  543. 'should truncate large arrays of jsonb and json to maxArraySize elements if their size is > maxCharacters',
  544. async (db) => {
  545. // Create test table with array column
  546. await db.executeQuery(`
  547. CREATE TABLE test_large_array_table (
  548. id SERIAL PRIMARY KEY,
  549. name TEXT,
  550. large_array_jsonb jsonb[],
  551. large_array_json json[]
  552. );
  553. -- Insert test data with a large array (>10KB)
  554. -- Create arrays with JSON objects
  555. INSERT INTO test_large_array_table (name, large_array_jsonb, large_array_json) VALUES
  556. (
  557. 'Large Array Item',
  558. (SELECT array_agg(jsonb_build_object(
  559. 'id', i,
  560. 'name', 'element_' || i,
  561. 'data', jsonb_build_object('value', i * 10, 'active', true)
  562. )) FROM generate_series(1, 1000) i),
  563. (SELECT array_agg(json_build_object(
  564. 'id', i,
  565. 'name', 'element_' || i,
  566. 'data', json_build_object('value', i * 10, 'active', true)
  567. )) FROM generate_series(1, 1000) i)
  568. ),
  569. (
  570. 'Large Array Small items',
  571. (SELECT array_agg(jsonb_build_object(
  572. 'id', i,
  573. 'value', i
  574. )) FROM generate_series(1, 100) i),
  575. (SELECT array_agg(json_build_object(
  576. 'id', i,
  577. 'value', i
  578. )) FROM generate_series(1, 100) i)
  579. ),
  580. (
  581. 'Normal Array Item',
  582. ARRAY[
  583. '{"id": 1, "tag": "tag1"}'::jsonb,
  584. '{"id": 2, "tag": "tag2"}'::jsonb,
  585. '{"id": 3, "tag": "tag3"}'::jsonb
  586. ],
  587. ARRAY[
  588. '{"id": 1, "tag": "tag1"}'::json,
  589. '{"id": 2, "tag": "tag2"}'::json,
  590. '{"id": 3, "tag": "tag3"}'::json
  591. ]
  592. );
  593. `)
  594. // Get table metadata
  595. const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
  596. const tables = tablesZod.parse(await db.executeQuery(tablesSql))
  597. const testTable = tables.find((table) => table.name === 'test_large_array_table')
  598. expect(testTable).toBeDefined()
  599. // Generate SQL with lower maxCharacters and maxArraySize limits
  600. const sql = getTableRowsSql({
  601. table: testTable!,
  602. page: 1,
  603. limit: 10,
  604. maxCharacters: 2048,
  605. maxArraySize: 10,
  606. })
  607. // Verify the SQL contains the array truncation logic
  608. expect(sql).toMatchInlineSnapshot(`
  609. "with _base_query as (select * from public.test_large_array_table order by test_large_array_table.id asc nulls last limit 10 offset 0)
  610. select id,case
  611. when octet_length(name::text) > 2048
  612. then left(name::text, 2048) || '...'
  613. else name::text
  614. end as name,
  615. case
  616. when octet_length(large_array_jsonb::text) > 2048
  617. then
  618. case
  619. when array_ndims(large_array_jsonb) = 1
  620. then
  621. (select array_cat(large_array_jsonb[1:10]::jsonb[], array['{"truncated": true}'::json]::jsonb[]))::jsonb[]
  622. else
  623. large_array_jsonb[1:10]::jsonb[]
  624. end
  625. else large_array_jsonb::jsonb[]
  626. end
  627. ,
  628. case
  629. when octet_length(large_array_json::text) > 2048
  630. then
  631. case
  632. when array_ndims(large_array_json) = 1
  633. then
  634. (select array_cat(large_array_json[1:10]::json[], array['{"truncated": true}'::json]::json[]))::json[]
  635. else
  636. large_array_json[1:10]::json[]
  637. end
  638. else large_array_json::json[]
  639. end
  640. from _base_query;"
  641. `)
  642. // Execute the SQL and verify results
  643. const queryResult = await db.executeQuery(sql)
  644. expect(queryResult).toMatchInlineSnapshot(`
  645. [
  646. {
  647. "id": 1,
  648. "large_array_json": [
  649. {
  650. "data": {
  651. "active": true,
  652. "value": 10,
  653. },
  654. "id": 1,
  655. "name": "element_1",
  656. },
  657. {
  658. "data": {
  659. "active": true,
  660. "value": 20,
  661. },
  662. "id": 2,
  663. "name": "element_2",
  664. },
  665. {
  666. "data": {
  667. "active": true,
  668. "value": 30,
  669. },
  670. "id": 3,
  671. "name": "element_3",
  672. },
  673. {
  674. "data": {
  675. "active": true,
  676. "value": 40,
  677. },
  678. "id": 4,
  679. "name": "element_4",
  680. },
  681. {
  682. "data": {
  683. "active": true,
  684. "value": 50,
  685. },
  686. "id": 5,
  687. "name": "element_5",
  688. },
  689. {
  690. "data": {
  691. "active": true,
  692. "value": 60,
  693. },
  694. "id": 6,
  695. "name": "element_6",
  696. },
  697. {
  698. "data": {
  699. "active": true,
  700. "value": 70,
  701. },
  702. "id": 7,
  703. "name": "element_7",
  704. },
  705. {
  706. "data": {
  707. "active": true,
  708. "value": 80,
  709. },
  710. "id": 8,
  711. "name": "element_8",
  712. },
  713. {
  714. "data": {
  715. "active": true,
  716. "value": 90,
  717. },
  718. "id": 9,
  719. "name": "element_9",
  720. },
  721. {
  722. "data": {
  723. "active": true,
  724. "value": 100,
  725. },
  726. "id": 10,
  727. "name": "element_10",
  728. },
  729. {
  730. "truncated": true,
  731. },
  732. ],
  733. "large_array_jsonb": [
  734. {
  735. "data": {
  736. "active": true,
  737. "value": 10,
  738. },
  739. "id": 1,
  740. "name": "element_1",
  741. },
  742. {
  743. "data": {
  744. "active": true,
  745. "value": 20,
  746. },
  747. "id": 2,
  748. "name": "element_2",
  749. },
  750. {
  751. "data": {
  752. "active": true,
  753. "value": 30,
  754. },
  755. "id": 3,
  756. "name": "element_3",
  757. },
  758. {
  759. "data": {
  760. "active": true,
  761. "value": 40,
  762. },
  763. "id": 4,
  764. "name": "element_4",
  765. },
  766. {
  767. "data": {
  768. "active": true,
  769. "value": 50,
  770. },
  771. "id": 5,
  772. "name": "element_5",
  773. },
  774. {
  775. "data": {
  776. "active": true,
  777. "value": 60,
  778. },
  779. "id": 6,
  780. "name": "element_6",
  781. },
  782. {
  783. "data": {
  784. "active": true,
  785. "value": 70,
  786. },
  787. "id": 7,
  788. "name": "element_7",
  789. },
  790. {
  791. "data": {
  792. "active": true,
  793. "value": 80,
  794. },
  795. "id": 8,
  796. "name": "element_8",
  797. },
  798. {
  799. "data": {
  800. "active": true,
  801. "value": 90,
  802. },
  803. "id": 9,
  804. "name": "element_9",
  805. },
  806. {
  807. "data": {
  808. "active": true,
  809. "value": 100,
  810. },
  811. "id": 10,
  812. "name": "element_10",
  813. },
  814. {
  815. "truncated": true,
  816. },
  817. ],
  818. "name": "Large Array Item",
  819. },
  820. {
  821. "id": 2,
  822. "large_array_json": [
  823. {
  824. "id": 1,
  825. "value": 1,
  826. },
  827. {
  828. "id": 2,
  829. "value": 2,
  830. },
  831. {
  832. "id": 3,
  833. "value": 3,
  834. },
  835. {
  836. "id": 4,
  837. "value": 4,
  838. },
  839. {
  840. "id": 5,
  841. "value": 5,
  842. },
  843. {
  844. "id": 6,
  845. "value": 6,
  846. },
  847. {
  848. "id": 7,
  849. "value": 7,
  850. },
  851. {
  852. "id": 8,
  853. "value": 8,
  854. },
  855. {
  856. "id": 9,
  857. "value": 9,
  858. },
  859. {
  860. "id": 10,
  861. "value": 10,
  862. },
  863. {
  864. "truncated": true,
  865. },
  866. ],
  867. "large_array_jsonb": [
  868. {
  869. "id": 1,
  870. "value": 1,
  871. },
  872. {
  873. "id": 2,
  874. "value": 2,
  875. },
  876. {
  877. "id": 3,
  878. "value": 3,
  879. },
  880. {
  881. "id": 4,
  882. "value": 4,
  883. },
  884. {
  885. "id": 5,
  886. "value": 5,
  887. },
  888. {
  889. "id": 6,
  890. "value": 6,
  891. },
  892. {
  893. "id": 7,
  894. "value": 7,
  895. },
  896. {
  897. "id": 8,
  898. "value": 8,
  899. },
  900. {
  901. "id": 9,
  902. "value": 9,
  903. },
  904. {
  905. "id": 10,
  906. "value": 10,
  907. },
  908. {
  909. "truncated": true,
  910. },
  911. ],
  912. "name": "Large Array Small items",
  913. },
  914. {
  915. "id": 3,
  916. "large_array_json": [
  917. {
  918. "id": 1,
  919. "tag": "tag1",
  920. },
  921. {
  922. "id": 2,
  923. "tag": "tag2",
  924. },
  925. {
  926. "id": 3,
  927. "tag": "tag3",
  928. },
  929. ],
  930. "large_array_jsonb": [
  931. {
  932. "id": 1,
  933. "tag": "tag1",
  934. },
  935. {
  936. "id": 2,
  937. "tag": "tag2",
  938. },
  939. {
  940. "id": 3,
  941. "tag": "tag3",
  942. },
  943. ],
  944. "name": "Normal Array Item",
  945. },
  946. ]
  947. `)
  948. }
  949. )
  950. withTestDatabase('should truncate fields to maxCharacters avoid', async (db) => {
  951. // Create test table with array column
  952. await db.executeQuery(`
  953. CREATE TABLE test_large_array_table (
  954. id SERIAL PRIMARY KEY,
  955. name TEXT,
  956. large_array TEXT[] -- Will hold a very large array
  957. );
  958. -- Insert test data with a large array (>10KB)
  959. -- Create an array with 1000 elements to ensure it exceeds 10KB
  960. INSERT INTO test_large_array_table (name, large_array) VALUES
  961. ('Normal Array Item', ARRAY['tag1', 'tag2', 'tag3']),
  962. -- Locally testing with up to 700 Mo in size should work and not raise a JS string alloc size error
  963. (repeat('A', 5 * 1024 * 1024), ARRAY['tag1', 'tag2', 'tag3']);
  964. `)
  965. // Get table metadata
  966. const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
  967. const tables = tablesZod.parse(await db.executeQuery(tablesSql))
  968. const testTable = tables.find((table) => table.name === 'test_large_array_table')
  969. expect(testTable).toBeDefined()
  970. // Generate SQL with lower maxCharacters and maxArraySize limits
  971. const sql = getTableRowsSql({
  972. table: testTable!,
  973. page: 1,
  974. limit: 10,
  975. maxCharacters: 256,
  976. })
  977. // Verify the SQL contains the array truncation logic
  978. expect(sql).toMatchInlineSnapshot(`
  979. "with _base_query as (select * from public.test_large_array_table order by test_large_array_table.id asc nulls last limit 10 offset 0)
  980. select id,case
  981. when octet_length(name::text) > 256
  982. then left(name::text, 256) || '...'
  983. else name::text
  984. end as name,
  985. case
  986. when octet_length(large_array::text) > 256
  987. then
  988. case
  989. when array_ndims(large_array) = 1
  990. then
  991. (select array_cat(large_array[1:50]::text[], array['...']::text[]))::text[]
  992. else
  993. large_array[1:50]::text[]
  994. end
  995. else large_array::text[]
  996. end
  997. from _base_query;"
  998. `)
  999. // Execute the SQL and verify results
  1000. const start = performance.now()
  1001. const queryResult = await db.executeQuery(sql)
  1002. const end = performance.now()
  1003. expect(end - start).lessThan(1000)
  1004. expect(queryResult).toMatchInlineSnapshot(`
  1005. [
  1006. {
  1007. "id": 1,
  1008. "large_array": [
  1009. "tag1",
  1010. "tag2",
  1011. "tag3",
  1012. ],
  1013. "name": "Normal Array Item",
  1014. },
  1015. {
  1016. "id": 2,
  1017. "large_array": [
  1018. "tag1",
  1019. "tag2",
  1020. "tag3",
  1021. ],
  1022. "name": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...",
  1023. },
  1024. ]
  1025. `)
  1026. })
  1027. withTestDatabase('should generate SQL with filtering', async (db) => {
  1028. // Create test table and insert data
  1029. await db.executeQuery(`
  1030. CREATE TABLE test_sql_filter (
  1031. id SERIAL PRIMARY KEY,
  1032. name TEXT,
  1033. category TEXT
  1034. );
  1035. -- Insert test data with different categories
  1036. INSERT INTO test_sql_filter (name, category) VALUES
  1037. ('Test Item 1', 'A'),
  1038. ('Test Item 2', 'B'),
  1039. ('Test Item 3', 'A'),
  1040. ('Another Item', 'A'),
  1041. ('Different Item', 'C');
  1042. `)
  1043. // Get table metadata
  1044. const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
  1045. const tables = tablesZod.parse(await db.executeQuery(tablesSql))
  1046. const testTable = tables.find((table) => table.name === 'test_sql_filter')
  1047. expect(testTable).toBeDefined()
  1048. // Define filters
  1049. const filters: Filter[] = [
  1050. { column: 'name', operator: '~~', value: 'Test%' },
  1051. { column: 'category', operator: '=', value: 'A' },
  1052. ]
  1053. // Generate SQL with filters
  1054. const sql = getTableRowsSql({
  1055. table: testTable!,
  1056. filters,
  1057. page: 1,
  1058. limit: 10,
  1059. })
  1060. // Verify SQL generation with snapshot
  1061. expect(sql).toMatchInlineSnapshot(
  1062. `
  1063. "with _base_query as (select * from public.test_sql_filter where name::text ~~ 'Test%' and category = 'A' order by test_sql_filter.id asc nulls last limit 10 offset 0)
  1064. select id,case
  1065. when octet_length(name::text) > 10240
  1066. then left(name::text, 10240) || '...'
  1067. else name::text
  1068. end as name,case
  1069. when octet_length(category::text) > 10240
  1070. then left(category::text, 10240) || '...'
  1071. else category::text
  1072. end as category from _base_query;"
  1073. `
  1074. )
  1075. // E2E Test: Execute the SQL and verify results
  1076. const queryResult = await db.executeQuery(sql)
  1077. expect(queryResult.length).toBe(2) // Should only get items that match both filters
  1078. expect(queryResult.map((row: any) => row.name)).toEqual(['Test Item 1', 'Test Item 3'])
  1079. expect(queryResult.every((row: any) => row.category === 'A')).toBe(true)
  1080. })
  1081. withTestDatabase('should generate SQL with sorting', async (db) => {
  1082. // Create test table and insert data
  1083. await db.executeQuery(`
  1084. CREATE TABLE test_sql_sort (
  1085. id SERIAL PRIMARY KEY,
  1086. name TEXT,
  1087. value INTEGER
  1088. );
  1089. -- Insert test data with varying values
  1090. INSERT INTO test_sql_sort (name, value) VALUES
  1091. ('Z Item', 10),
  1092. ('A Item', 30),
  1093. ('M Item', 20),
  1094. ('X Item', null);
  1095. `)
  1096. // Get table metadata
  1097. const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
  1098. const tables = tablesZod.parse(await db.executeQuery(tablesSql))
  1099. const testTable = tables.find((table) => table.name === 'test_sql_sort')
  1100. expect(testTable).toBeDefined()
  1101. // Define sorts
  1102. const sorts: Sort[] = [
  1103. { column: 'name', table: 'test_sql_sort', ascending: true, nullsFirst: false },
  1104. { column: 'value', table: 'test_sql_sort', ascending: false, nullsFirst: true },
  1105. ]
  1106. // Generate SQL with sorting
  1107. const sql = getTableRowsSql({
  1108. table: testTable!,
  1109. sorts,
  1110. page: 1,
  1111. limit: 10,
  1112. })
  1113. // Verify SQL generation with snapshot
  1114. expect(sql).toMatchInlineSnapshot(
  1115. `
  1116. "with _base_query as (select * from public.test_sql_sort order by test_sql_sort.name asc nulls last, test_sql_sort.value desc nulls first limit 10 offset 0)
  1117. select id,case
  1118. when octet_length(name::text) > 10240
  1119. then left(name::text, 10240) || '...'
  1120. else name::text
  1121. end as name,value from _base_query;"
  1122. `
  1123. )
  1124. // E2E Test: Execute the SQL and verify results
  1125. const queryResult = await db.executeQuery(sql)
  1126. expect(queryResult.length).toBe(4)
  1127. // Should be sorted by name (asc) first, then by value (desc, nulls first)
  1128. expect(queryResult.map((row: any) => row.name)).toEqual([
  1129. 'A Item',
  1130. 'M Item',
  1131. 'X Item',
  1132. 'Z Item',
  1133. ])
  1134. // The first item (A Item) should have value 30
  1135. expect(queryResult[0].value).toBe(30)
  1136. // Check if the X Item (with null value) is before Z Item (non-null value)
  1137. // due to nullsFirst: true for the value sort
  1138. const xItemIndex = queryResult.findIndex((row: any) => row.name === 'X Item')
  1139. const zItemIndex = queryResult.findIndex((row: any) => row.name === 'Z Item')
  1140. expect(xItemIndex).toBeLessThan(zItemIndex)
  1141. })
  1142. withTestDatabase('should generate SQL for special/quoted column names', async (db) => {
  1143. // Create test table with quoted names and insert data
  1144. await db.executeQuery(`
  1145. CREATE TABLE "test sql spaces" (
  1146. id SERIAL PRIMARY KEY,
  1147. "user name" TEXT,
  1148. "column-with-dashes" TEXT,
  1149. "quoted""column" TEXT
  1150. );
  1151. -- Insert test data
  1152. INSERT INTO "test sql spaces" ("user name", "column-with-dashes", "quoted""column") VALUES
  1153. ('User 1', 'Value 1', 'Quoted 1'),
  1154. ('User 2', 'Value 2', 'Quoted 2');
  1155. `)
  1156. // Get table metadata
  1157. const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
  1158. const tables = tablesZod.parse(await db.executeQuery(tablesSql))
  1159. const testTable = tables.find((table) => table.name === 'test sql spaces')
  1160. expect(testTable).toBeDefined()
  1161. // Generate SQL
  1162. const sql = getTableRowsSql({
  1163. table: testTable!,
  1164. page: 1,
  1165. limit: 10,
  1166. })
  1167. // Verify SQL generation with snapshot
  1168. expect(sql).toMatchInlineSnapshot(
  1169. `
  1170. "with _base_query as (select * from public."test sql spaces" order by "test sql spaces".id asc nulls last limit 10 offset 0)
  1171. select id,case
  1172. when octet_length("user name"::text) > 10240
  1173. then left("user name"::text, 10240) || '...'
  1174. else "user name"::text
  1175. end as "user name",case
  1176. when octet_length("column-with-dashes"::text) > 10240
  1177. then left("column-with-dashes"::text, 10240) || '...'
  1178. else "column-with-dashes"::text
  1179. end as "column-with-dashes",case
  1180. when octet_length("quoted""column"::text) > 10240
  1181. then left("quoted""column"::text, 10240) || '...'
  1182. else "quoted""column"::text
  1183. end as "quoted""column" from _base_query;"
  1184. `
  1185. )
  1186. // E2E Test: Execute the SQL and verify results
  1187. const queryResult = await db.executeQuery(sql)
  1188. expect(queryResult.length).toBe(2)
  1189. expect(queryResult.map((row: any) => row['user name'])).toEqual(['User 1', 'User 2'])
  1190. expect(queryResult.map((row: any) => row['column-with-dashes'])).toEqual([
  1191. 'Value 1',
  1192. 'Value 2',
  1193. ])
  1194. expect(queryResult.map((row: any) => row['quoted"column'])).toEqual(['Quoted 1', 'Quoted 2'])
  1195. })
  1196. withTestDatabase('should generate SQL for tables with large text fields', async (db) => {
  1197. // Create test table with large text fields
  1198. await db.executeQuery(`
  1199. CREATE TABLE test_large_text (
  1200. id SERIAL PRIMARY KEY,
  1201. small_text VARCHAR(100),
  1202. large_text TEXT,
  1203. json_data JSONB
  1204. );
  1205. -- Insert test data including a large text field
  1206. INSERT INTO test_large_text (small_text, large_text, json_data) VALUES
  1207. ('Small text', repeat('Lorem ipsum ', 100), '{"key": "value", "nested": {"data": true}}'),
  1208. ('Another small text', repeat('Dolor sit amet ', 100), '{"array": [1, 2, 3], "bool": false}');
  1209. `)
  1210. // Get table metadata
  1211. const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
  1212. const tables = tablesZod.parse(await db.executeQuery(tablesSql))
  1213. const testTable = tables.find((table) => table.name === 'test_large_text')
  1214. expect(testTable).toBeDefined()
  1215. // Generate SQL
  1216. const sql = getTableRowsSql({
  1217. table: testTable!,
  1218. page: 1,
  1219. limit: 10,
  1220. })
  1221. // Verify SQL generation with snapshot
  1222. expect(sql).toMatchInlineSnapshot(
  1223. `
  1224. "with _base_query as (select * from public.test_large_text order by test_large_text.id asc nulls last limit 10 offset 0)
  1225. select id,case
  1226. when octet_length(small_text::text) > 10240
  1227. then left(small_text::text, 10240) || '...'
  1228. else small_text::text
  1229. end as small_text,case
  1230. when octet_length(large_text::text) > 10240
  1231. then left(large_text::text, 10240) || '...'
  1232. else large_text::text
  1233. end as large_text,case
  1234. when octet_length(json_data::text) > 10240
  1235. then left(json_data::text, 10240) || '...'
  1236. else json_data::text
  1237. end as json_data from _base_query;"
  1238. `
  1239. )
  1240. // E2E Test: Execute the SQL and verify results
  1241. const queryResult = await db.executeQuery(sql)
  1242. expect(queryResult.length).toBe(2)
  1243. expect(queryResult.map((row: any) => row.small_text)).toEqual([
  1244. 'Small text',
  1245. 'Another small text',
  1246. ])
  1247. expect(queryResult[0].large_text.startsWith('Lorem ipsum')).toBe(true)
  1248. expect(queryResult[1].large_text.startsWith('Dolor sit amet')).toBe(true)
  1249. expect(JSON.parse(queryResult[0].json_data)).toHaveProperty('key', 'value')
  1250. expect(JSON.parse(queryResult[1].json_data)).toHaveProperty('array')
  1251. })
  1252. withTestDatabase('should generate SQL with pagination', async (db) => {
  1253. // Create test table and insert multiple rows for pagination
  1254. await db.executeQuery(`
  1255. CREATE TABLE test_pagination (
  1256. id SERIAL PRIMARY KEY,
  1257. name TEXT
  1258. );
  1259. -- Insert 15 rows for pagination testing
  1260. INSERT INTO test_pagination (name)
  1261. SELECT 'Item ' || i FROM generate_series(1, 15) i;
  1262. `)
  1263. // Get table metadata
  1264. const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
  1265. const tables = tablesZod.parse(await db.executeQuery(tablesSql))
  1266. const testTable = tables.find((table) => table.name === 'test_pagination')
  1267. expect(testTable).toBeDefined()
  1268. // Generate SQL for page 1 (5 items)
  1269. const sql1 = getTableRowsSql({
  1270. table: testTable!,
  1271. page: 1,
  1272. limit: 5,
  1273. })
  1274. expect(sql1).toMatchInlineSnapshot(
  1275. `
  1276. "with _base_query as (select * from public.test_pagination order by test_pagination.id asc nulls last limit 5 offset 0)
  1277. select id,case
  1278. when octet_length(name::text) > 10240
  1279. then left(name::text, 10240) || '...'
  1280. else name::text
  1281. end as name from _base_query;"
  1282. `
  1283. )
  1284. const page1Result = await db.executeQuery(sql1)
  1285. expect(page1Result.length).toBe(5)
  1286. expect(page1Result.map((row: any) => row.name)).toEqual([
  1287. 'Item 1',
  1288. 'Item 2',
  1289. 'Item 3',
  1290. 'Item 4',
  1291. 'Item 5',
  1292. ])
  1293. const sql2 = getTableRowsSql({
  1294. table: testTable!,
  1295. page: 2,
  1296. limit: 5,
  1297. })
  1298. // Verify SQL generation for page 2
  1299. expect(sql2).toMatchInlineSnapshot(
  1300. `
  1301. "with _base_query as (select * from public.test_pagination order by test_pagination.id asc nulls last limit 5 offset 5)
  1302. select id,case
  1303. when octet_length(name::text) > 10240
  1304. then left(name::text, 10240) || '...'
  1305. else name::text
  1306. end as name from _base_query;"
  1307. `
  1308. )
  1309. const page2Result = await db.executeQuery(sql2)
  1310. expect(page2Result.length).toBe(5)
  1311. expect(page2Result.map((row: any) => row.name)).toEqual([
  1312. 'Item 6',
  1313. 'Item 7',
  1314. 'Item 8',
  1315. 'Item 9',
  1316. 'Item 10',
  1317. ])
  1318. })
  1319. withTestDatabase('should generate SQL for view', async (db) => {
  1320. // Create table and view
  1321. await db.executeQuery(`
  1322. CREATE TABLE test_view_source (
  1323. id SERIAL PRIMARY KEY,
  1324. name TEXT,
  1325. active BOOLEAN
  1326. );
  1327. -- Insert test data
  1328. INSERT INTO test_view_source (name, active) VALUES
  1329. ('Active Item 1', true),
  1330. ('Inactive Item', false),
  1331. ('Active Item 2', true);
  1332. -- Create view that only shows active items
  1333. CREATE VIEW test_sql_view AS
  1334. SELECT id, name, active FROM test_view_source WHERE active = true;
  1335. `)
  1336. // Get view metadata
  1337. const { sql: viewsSql, zod: viewsZod } = pgMeta.views.list()
  1338. const views = viewsZod.parse(await db.executeQuery(viewsSql))
  1339. const testView = views.find((view) => view.name === 'test_sql_view')
  1340. expect(testView).toBeDefined()
  1341. // Generate SQL
  1342. const sql = getTableRowsSql({
  1343. table: testView!,
  1344. page: 1,
  1345. limit: 10,
  1346. })
  1347. // Verify SQL generation with snapshot
  1348. expect(sql).toMatchInlineSnapshot(
  1349. `
  1350. "with _base_query as (select * from public.test_sql_view order by test_sql_view.id asc nulls last limit 10 offset 0)
  1351. select id,case
  1352. when octet_length(name::text) > 10240
  1353. then left(name::text, 10240) || '...'
  1354. else name::text
  1355. end as name,active from _base_query;"
  1356. `
  1357. )
  1358. // E2E Test: Execute the SQL and verify results
  1359. const queryResult = await db.executeQuery(sql)
  1360. expect(queryResult.length).toBe(2) // Only active items should be in the view
  1361. expect(queryResult.map((row: any) => row.name)).toEqual(['Active Item 1', 'Active Item 2'])
  1362. expect(queryResult.every((row: any) => row.active === true)).toBe(true)
  1363. })
  1364. withTestDatabase('should generate SQL for materialized view', async (db) => {
  1365. // Create table and materialized view
  1366. await db.executeQuery(`
  1367. CREATE TABLE test_mv_source (
  1368. id SERIAL PRIMARY KEY,
  1369. name TEXT,
  1370. value NUMERIC
  1371. );
  1372. -- Insert test data
  1373. INSERT INTO test_mv_source (name, value) VALUES
  1374. ('Item 1', 10.5),
  1375. ('Item 2', -5.25),
  1376. ('Item 3', 20);
  1377. -- Create materialized view that only includes positive values
  1378. CREATE MATERIALIZED VIEW test_sql_mv AS
  1379. SELECT id, name, value FROM test_mv_source WHERE value > 0;
  1380. `)
  1381. // Get materialized view metadata
  1382. const { sql: mvSql, zod: mvZod } = pgMeta.materializedViews.list()
  1383. const materializedViews = mvZod.parse(await db.executeQuery(mvSql))
  1384. const testMv = materializedViews.find((mv) => mv.name === 'test_sql_mv')
  1385. expect(testMv).toBeDefined()
  1386. // Generate SQL
  1387. const sql = getTableRowsSql({
  1388. table: testMv!,
  1389. page: 1,
  1390. limit: 10,
  1391. })
  1392. // Verify SQL generation with snapshot
  1393. expect(sql).toMatchInlineSnapshot(
  1394. `
  1395. "with _base_query as (select * from public.test_sql_mv order by test_sql_mv.id asc nulls last limit 10 offset 0)
  1396. select id,case
  1397. when octet_length(name::text) > 10240
  1398. then left(name::text, 10240) || '...'
  1399. else name::text
  1400. end as name,value from _base_query;"
  1401. `
  1402. )
  1403. // E2E Test: Execute the SQL and verify results
  1404. const queryResult = await db.executeQuery(sql)
  1405. expect(queryResult.length).toBe(2) // Only items with positive values
  1406. expect(queryResult.map((row: any) => row.name)).toEqual(['Item 1', 'Item 3'])
  1407. expect(queryResult.every((row: any) => row.value > 0)).toBe(true)
  1408. })
  1409. withTestDatabase('should generate SQL for foreign table', async (db) => {
  1410. // Set up a foreign table with the file_fdw extension
  1411. await db.executeQuery(`
  1412. -- Create the extension if it doesn't exist
  1413. CREATE EXTENSION IF NOT EXISTS file_fdw;
  1414. -- Create a foreign server
  1415. DROP SERVER IF EXISTS file_server2 CASCADE;
  1416. CREATE SERVER file_server2 FOREIGN DATA WRAPPER file_fdw;
  1417. -- Create a table to export data from
  1418. CREATE TABLE source_for_foreign_test (
  1419. id SERIAL PRIMARY KEY,
  1420. name TEXT,
  1421. description TEXT
  1422. );
  1423. -- Insert test data
  1424. INSERT INTO source_for_foreign_test (name, description) VALUES
  1425. ('Foreign Item 1', 'Description 1'),
  1426. ('Foreign Item 2', 'Description 2');
  1427. -- Export to CSV for the foreign table
  1428. COPY source_for_foreign_test TO '/tmp/foreign_test2.csv' WITH (FORMAT csv, HEADER);
  1429. -- Create the foreign table
  1430. CREATE FOREIGN TABLE test_sql_foreign (
  1431. id INT,
  1432. name TEXT,
  1433. description TEXT
  1434. ) SERVER file_server2
  1435. OPTIONS (filename '/tmp/foreign_test2.csv', format 'csv', header 'true');
  1436. `)
  1437. // Get foreign table metadata
  1438. const { sql: ftSql, zod: ftZod } = pgMeta.foreignTables.list()
  1439. const foreignTables = ftZod.parse(await db.executeQuery(ftSql))
  1440. const testFt = foreignTables.find((ft) => ft.name === 'test_sql_foreign')
  1441. expect(testFt).toBeDefined()
  1442. // Generate SQL
  1443. const sql = getTableRowsSql({
  1444. table: testFt!,
  1445. page: 1,
  1446. limit: 10,
  1447. })
  1448. // Verify SQL generation with snapshot
  1449. expect(sql).toMatchInlineSnapshot(
  1450. `
  1451. "with _base_query as (select * from public.test_sql_foreign order by test_sql_foreign.id asc nulls last limit 10 offset 0)
  1452. select id,case
  1453. when octet_length(name::text) > 10240
  1454. then left(name::text, 10240) || '...'
  1455. else name::text
  1456. end as name,case
  1457. when octet_length(description::text) > 10240
  1458. then left(description::text, 10240) || '...'
  1459. else description::text
  1460. end as description from _base_query;"
  1461. `
  1462. )
  1463. // E2E Test: Execute the SQL and verify results
  1464. const queryResult = await db.executeQuery(sql)
  1465. expect(queryResult).toMatchInlineSnapshot(`
  1466. [
  1467. {
  1468. "description": "Description 1",
  1469. "id": 1,
  1470. "name": "Foreign Item 1",
  1471. },
  1472. {
  1473. "description": "Description 2",
  1474. "id": 2,
  1475. "name": "Foreign Item 2",
  1476. },
  1477. ]
  1478. `)
  1479. })
  1480. })
  1481. withTestDatabase('should handle large multi-dimensional arrays correctly', async (db) => {
  1482. // Create test table with multi-dimensional arrays
  1483. await db.executeQuery(`
  1484. CREATE TABLE public.monitor_data (
  1485. subject_id TEXT,
  1486. "timestamp" TIMESTAMP[],
  1487. "PPG" FLOAT8[][],
  1488. "ACC" FLOAT8[][]
  1489. );
  1490. INSERT INTO public.monitor_data (subject_id, "timestamp", "PPG", "ACC")
  1491. VALUES (
  1492. 'subject-1',
  1493. ARRAY['2024-01-01 00:00:00'::timestamp, '2024-01-02 00:00:00'::timestamp],
  1494. ARRAY[
  1495. [1.1, 1.2, 1.3, 1.4, 1.5, 1.6],
  1496. [2.1, 2.2, 2.3, 2.4, 2.5, 2.6],
  1497. [3.1, 3.2, 3.3, 3.4, 3.5, 3.6]
  1498. ]::FLOAT8[][],
  1499. ARRAY[
  1500. [4.1, 4.2, 4.3, 4.4, 4.5, 4.6],
  1501. [5.1, 5.2, 5.3, 5.4, 5.5, 5.6],
  1502. [6.1, 6.2, 6.3, 6.4, 6.5, 6.6]
  1503. ]::FLOAT8[][]
  1504. );
  1505. INSERT INTO public.monitor_data (subject_id, "timestamp", "PPG", "ACC")
  1506. VALUES (
  1507. 'subject-large',
  1508. -- large 1D timestamp array (e.g., 1000 timestamps)
  1509. ARRAY(
  1510. SELECT generate_series('2024-01-01'::timestamp, '2024-01-01'::timestamp + interval '999 minutes', '1 minute')
  1511. ),
  1512. -- large 2D float8 arrays (e.g., 1000 x 6)
  1513. ARRAY(
  1514. SELECT ARRAY[
  1515. random(), random(), random(), random(), random(), random()
  1516. ]::float8[]
  1517. FROM generate_series(1, 1000)
  1518. )::float8[][],
  1519. ARRAY(
  1520. SELECT ARRAY[
  1521. random(), random(), random(), random(), random(), random()
  1522. ]::float8[]
  1523. FROM generate_series(1, 1000)
  1524. )::float8[][]
  1525. );
  1526. `)
  1527. // Get table metadata
  1528. const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
  1529. const tables = tablesZod.parse(await db.executeQuery(tablesSql))
  1530. const testTable = tables.find((table) => table.name === 'monitor_data')
  1531. expect(testTable).toBeDefined()
  1532. // Generate SQL with default settings
  1533. const sql = getTableRowsSql({
  1534. table: testTable!,
  1535. page: 1,
  1536. limit: 10,
  1537. })
  1538. // Verify SQL generation with snapshot
  1539. expect(sql).toMatchInlineSnapshot(`
  1540. "with _base_query as (select * from public.monitor_data order by monitor_data.subject_id asc nulls last limit 10 offset 0)
  1541. select case
  1542. when octet_length(subject_id::text) > 10240
  1543. then left(subject_id::text, 10240) || '...'
  1544. else subject_id::text
  1545. end as subject_id,
  1546. case
  1547. when octet_length("timestamp"::text) > 10240
  1548. then
  1549. case
  1550. when array_ndims("timestamp") = 1
  1551. then
  1552. (select array_cat("timestamp"[1:50]::text[], array['...']::text[]))::text[]
  1553. else
  1554. "timestamp"[1:50]::text[]
  1555. end
  1556. else "timestamp"::text[]
  1557. end
  1558. ,
  1559. case
  1560. when octet_length("PPG"::text) > 10240
  1561. then
  1562. case
  1563. when array_ndims("PPG") = 1
  1564. then
  1565. (select array_cat("PPG"[1:50]::text[], array['...']::text[]))::text[]
  1566. else
  1567. "PPG"[1:50]::text[]
  1568. end
  1569. else "PPG"::text[]
  1570. end
  1571. ,
  1572. case
  1573. when octet_length("ACC"::text) > 10240
  1574. then
  1575. case
  1576. when array_ndims("ACC") = 1
  1577. then
  1578. (select array_cat("ACC"[1:50]::text[], array['...']::text[]))::text[]
  1579. else
  1580. "ACC"[1:50]::text[]
  1581. end
  1582. else "ACC"::text[]
  1583. end
  1584. from _base_query;"
  1585. `)
  1586. // Execute the SQL and verify results
  1587. const queryResult = await db.executeQuery(sql)
  1588. expect(queryResult.length).toBe(2)
  1589. // Verify the first row (small arrays)
  1590. const smallRow = queryResult.find((row: any) => row.subject_id === 'subject-1')
  1591. expect(smallRow).toBeDefined()
  1592. expect(smallRow.timestamp).toHaveLength(2)
  1593. expect(smallRow.PPG).toHaveLength(3)
  1594. expect(smallRow.PPG[0]).toHaveLength(6)
  1595. expect(smallRow.ACC).toHaveLength(3)
  1596. expect(smallRow.ACC[0]).toHaveLength(6)
  1597. // Verify the second row (large arrays)
  1598. const largeRow = queryResult.find((row: any) => row.subject_id === 'subject-large')
  1599. expect(largeRow).toBeDefined()
  1600. expect(largeRow.timestamp).toHaveLength(51) // Has the extra '...' element
  1601. expect(largeRow.PPG).toHaveLength(50)
  1602. expect(largeRow.PPG[0]).toHaveLength(6)
  1603. expect(largeRow.ACC).toHaveLength(50)
  1604. expect(largeRow.ACC[0]).toHaveLength(6)
  1605. // Test with custom maxArraySize
  1606. const sqlWithCustomSize = getTableRowsSql({
  1607. table: testTable!,
  1608. page: 1,
  1609. limit: 10,
  1610. maxArraySize: 10,
  1611. })
  1612. const customSizeResult = await db.executeQuery(sqlWithCustomSize)
  1613. const largeRowCustom = customSizeResult.find((row: any) => row.subject_id === 'subject-large')
  1614. expect(largeRowCustom.timestamp).toHaveLength(11) // Has the extra '...' element
  1615. expect(largeRowCustom.PPG).toHaveLength(10) // multi-dimentional array are truncated
  1616. expect(largeRowCustom.ACC).toHaveLength(10)
  1617. })
  1618. withTestDatabase('should handle reserved keyword "collation" as column name', async (db) => {
  1619. // Create a table with a column named "collation" (a PostgreSQL reserved keyword)
  1620. await db.executeQuery(`
  1621. CREATE TABLE IF NOT EXISTS "public"."test" (
  1622. id SERIAL PRIMARY KEY,
  1623. "collation" TEXT
  1624. );
  1625. DELETE FROM "public"."test";
  1626. INSERT INTO "public"."test" ("collation")
  1627. VALUES
  1628. ('value1'),
  1629. ('value2'),
  1630. ('value3');
  1631. `)
  1632. // Get table metadata
  1633. const { sql: tablesSql, zod: tablesZod } = pgMeta.tables.list()
  1634. const tables = tablesZod.parse(await db.executeQuery(tablesSql))
  1635. const testTable = tables.find((table) => table.name === 'test')
  1636. expect(testTable).toBeDefined()
  1637. // Generate SQL - this should properly quote the "collation" column name
  1638. const sql = getTableRowsSql({
  1639. table: testTable!,
  1640. page: 1,
  1641. limit: 100,
  1642. })
  1643. // Verify SQL generation - the "collation" column should be properly quoted
  1644. expect(sql).toMatchInlineSnapshot(`
  1645. "with _base_query as (select * from public.test order by test.id asc nulls last limit 100 offset 0)
  1646. select id,case
  1647. when octet_length("collation"::text) > 10240
  1648. then left("collation"::text, 10240) || '...'
  1649. else "collation"::text
  1650. end as "collation" from _base_query;"
  1651. `)
  1652. // Execute the generated SQL and verify the results
  1653. const queryResult = await db.executeQuery(sql)
  1654. expect(queryResult.length).toBe(3)
  1655. expect(queryResult[0].collation).toBe('value1')
  1656. expect(queryResult[1].collation).toBe('value2')
  1657. expect(queryResult[2].collation).toBe('value3')
  1658. // Test with ORDER BY on the collation column
  1659. const sqlWithOrder = getTableRowsSql({
  1660. table: testTable!,
  1661. page: 1,
  1662. limit: 100,
  1663. sorts: [{ table: 'test', column: 'collation', ascending: true, nullsFirst: false }],
  1664. })
  1665. // Verify the ORDER BY clause properly quotes the collation column
  1666. expect(sqlWithOrder).toMatchInlineSnapshot(`
  1667. "with _base_query as (select * from public.test order by test."collation" asc nulls last limit 100 offset 0)
  1668. select id,case
  1669. when octet_length("collation"::text) > 10240
  1670. then left("collation"::text, 10240) || '...'
  1671. else "collation"::text
  1672. end as "collation" from _base_query;"
  1673. `)
  1674. const queryResultWithOrder = await db.executeQuery(sqlWithOrder)
  1675. expect(queryResultWithOrder.length).toBe(3)
  1676. expect(queryResultWithOrder[0].collation).toBe('value1')
  1677. expect(queryResultWithOrder[1].collation).toBe('value2')
  1678. expect(queryResultWithOrder[2].collation).toBe('value3')
  1679. // Test with FILTER on the collation column
  1680. const sqlWithFilter = getTableRowsSql({
  1681. table: testTable!,
  1682. page: 1,
  1683. limit: 100,
  1684. filters: [{ column: 'collation', operator: '=', value: 'value2' }],
  1685. })
  1686. // Verify the WHERE clause properly quotes the collation column
  1687. expect(sqlWithFilter).toMatchInlineSnapshot(`
  1688. "with _base_query as (select * from public.test where "collation" = 'value2' order by test.id asc nulls last limit 100 offset 0)
  1689. select id,case
  1690. when octet_length("collation"::text) > 10240
  1691. then left("collation"::text, 10240) || '...'
  1692. else "collation"::text
  1693. end as "collation" from _base_query;"
  1694. `)
  1695. const queryResultWithFilter = await db.executeQuery(sqlWithFilter)
  1696. expect(queryResultWithFilter.length).toBe(1)
  1697. expect(queryResultWithFilter[0].collation).toBe('value2')
  1698. })
  1699. })