useQueryPerformanceQuery.test.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import { describe, expect, it } from 'vitest'
  2. import { QueryPerformanceSort } from './QueryPerformance.types'
  3. import { generateQueryPerformanceSql } from './useQueryPerformanceQuery'
  4. describe('generateQueryPerformanceSql', () => {
  5. it('generates sql with no filters', () => {
  6. const result = generateQueryPerformanceSql({ preset: 'mostFrequentlyInvoked' })
  7. expect(result.sql).toBeDefined()
  8. expect(result.whereSql).toBe('')
  9. expect(result.orderBySql).toBeUndefined()
  10. })
  11. it('generates ORDER BY clause', () => {
  12. const result = generateQueryPerformanceSql({
  13. preset: 'mostFrequentlyInvoked',
  14. orderBy: { column: 'calls', order: 'desc' },
  15. })
  16. expect(result.orderBySql).toBe('ORDER BY calls desc')
  17. })
  18. it('filters by roles', () => {
  19. const result = generateQueryPerformanceSql({
  20. preset: 'mostFrequentlyInvoked',
  21. roles: ['postgres', 'anon'],
  22. })
  23. expect(result.whereSql).toContain("auth.rolname in ('postgres', 'anon')")
  24. })
  25. it('filters by search query', () => {
  26. const result = generateQueryPerformanceSql({
  27. preset: 'mostFrequentlyInvoked',
  28. searchQuery: 'SELECT',
  29. })
  30. expect(result.whereSql).toContain("statements.query ~* 'SELECT'")
  31. })
  32. it('filters by dashboard source only', () => {
  33. const result = generateQueryPerformanceSql({
  34. preset: 'mostFrequentlyInvoked',
  35. sources: ['dashboard'],
  36. })
  37. expect(result.whereSql).toContain("statements.query ~* 'source: dashboard'")
  38. })
  39. it('filters by non-dashboard source only', () => {
  40. const result = generateQueryPerformanceSql({
  41. preset: 'mostFrequentlyInvoked',
  42. sources: ['non-dashboard'],
  43. })
  44. expect(result.whereSql).toContain("statements.query !~* 'source: dashboard'")
  45. })
  46. it('does not add source filter when both sources are selected', () => {
  47. const result = generateQueryPerformanceSql({
  48. preset: 'mostFrequentlyInvoked',
  49. sources: ['dashboard', 'non-dashboard'],
  50. })
  51. expect(result.whereSql).not.toContain('source: dashboard')
  52. })
  53. it('filters by minimum calls', () => {
  54. const result = generateQueryPerformanceSql({
  55. preset: 'mostFrequentlyInvoked',
  56. minCalls: 10,
  57. })
  58. expect(result.whereSql).toContain('statements.calls >= 10')
  59. })
  60. it('does not filter by minCalls when 0', () => {
  61. const result = generateQueryPerformanceSql({
  62. preset: 'mostFrequentlyInvoked',
  63. minCalls: 0,
  64. })
  65. expect(result.whereSql).not.toContain('statements.calls')
  66. })
  67. it('filters by minimum total time', () => {
  68. const result = generateQueryPerformanceSql({
  69. preset: 'mostFrequentlyInvoked',
  70. minTotalTime: 500,
  71. })
  72. expect(result.whereSql).toContain(
  73. '(statements.total_exec_time + statements.total_plan_time) >= 500'
  74. )
  75. })
  76. it('combines multiple WHERE conditions with AND', () => {
  77. const result = generateQueryPerformanceSql({
  78. preset: 'mostFrequentlyInvoked',
  79. roles: ['postgres'],
  80. searchQuery: 'SELECT',
  81. minCalls: 5,
  82. })
  83. expect(result.whereSql).toContain("auth.rolname in ('postgres')")
  84. expect(result.whereSql).toContain("statements.query ~* 'SELECT'")
  85. expect(result.whereSql).toContain('statements.calls >= 5')
  86. expect(result.whereSql.split(' AND ').length).toBe(3)
  87. })
  88. it('passes WHERE clause to base sql function', () => {
  89. const result = generateQueryPerformanceSql({
  90. preset: 'mostFrequentlyInvoked',
  91. roles: ['postgres'],
  92. })
  93. expect(result.sql).toContain('WHERE')
  94. })
  95. it('does not include user-defined WHERE conditions when no filters set', () => {
  96. const result = generateQueryPerformanceSql({ preset: 'mostFrequentlyInvoked' })
  97. expect(result.whereSql).toBe('')
  98. // The base SQL may still contain its own WHERE clause (e.g. statements.calls > 0)
  99. // but the user-defined whereSql should be empty
  100. })
  101. it('passes runIndexAdvisor and filterIndexAdvisor flags', () => {
  102. const resultWithAdvisor = generateQueryPerformanceSql({
  103. preset: 'mostFrequentlyInvoked',
  104. runIndexAdvisor: true,
  105. filterIndexAdvisor: true,
  106. })
  107. expect(resultWithAdvisor.sql).toBeDefined()
  108. })
  109. it('works with different presets', () => {
  110. const presets = [
  111. 'mostFrequentlyInvoked',
  112. 'mostTimeConsuming',
  113. 'slowestExecutionTime',
  114. 'queryHitRate',
  115. 'unified',
  116. ] as const
  117. for (const preset of presets) {
  118. const result = generateQueryPerformanceSql({ preset })
  119. expect(result.sql).toBeDefined()
  120. }
  121. })
  122. it('clamps page=0 to page=1 (no negative offset)', () => {
  123. const result = generateQueryPerformanceSql({ preset: 'unified', page: 0, pageSize: 20 })
  124. expect(result.sql).toContain('offset 0')
  125. expect(result.sql).not.toMatch(/offset -\d/)
  126. })
  127. it('clamps negative page to page=1', () => {
  128. const result = generateQueryPerformanceSql({ preset: 'unified', page: -5, pageSize: 20 })
  129. expect(result.sql).toContain('offset 0')
  130. })
  131. it('clamps pageSize above 100 to 100', () => {
  132. const result = generateQueryPerformanceSql({ preset: 'unified', page: 1, pageSize: 9999 })
  133. expect(result.sql).toContain('limit 100')
  134. expect(result.sql).not.toContain('limit 9999')
  135. })
  136. it('applies LIMIT and OFFSET for page 2', () => {
  137. const result = generateQueryPerformanceSql({ preset: 'unified', page: 2, pageSize: 20 })
  138. expect(result.sql).toContain('limit 20 offset 20')
  139. })
  140. it('does not produce NaN in SQL when page is NaN', () => {
  141. const result = generateQueryPerformanceSql({ preset: 'unified', page: NaN, pageSize: 20 })
  142. expect(result.sql).not.toContain('NaN')
  143. expect(result.sql).toContain('offset 0')
  144. })
  145. it('does not produce NaN in SQL when pageSize is NaN', () => {
  146. const result = generateQueryPerformanceSql({ preset: 'unified', page: 1, pageSize: NaN })
  147. expect(result.sql).not.toContain('NaN')
  148. expect(result.sql).toContain('limit 20')
  149. })
  150. })
  151. describe('generateQueryPerformanceSql - ORDER BY column validation', () => {
  152. it('rejects invalid orderBy column containing colon (old URL format)', () => {
  153. const result = generateQueryPerformanceSql({
  154. preset: 'mostFrequentlyInvoked',
  155. orderBy: { column: 'created_at:asc' as any, order: 'desc' },
  156. })
  157. expect(result.orderBySql).toBeUndefined()
  158. })
  159. it('rejects SQL injection in orderBy column', () => {
  160. const result = generateQueryPerformanceSql({
  161. preset: 'mostFrequentlyInvoked',
  162. orderBy: { column: 'calls; DROP TABLE users--' as any, order: 'asc' },
  163. })
  164. expect(result.orderBySql).toBeUndefined()
  165. expect(result.sql).not.toContain('DROP TABLE')
  166. })
  167. it('falls back to default ORDER BY when column is invalid', () => {
  168. const result = generateQueryPerformanceSql({
  169. preset: 'mostFrequentlyInvoked',
  170. orderBy: { column: 'created_at:asc' as any, order: 'asc' },
  171. })
  172. expect(result.sql).toContain('order by statements.calls desc')
  173. })
  174. it('accepts all valid sort columns', () => {
  175. const columns: QueryPerformanceSort['column'][] = [
  176. 'query',
  177. 'rolname',
  178. 'total_time',
  179. 'prop_total_time',
  180. 'calls',
  181. 'avg_rows',
  182. 'max_time',
  183. 'mean_time',
  184. 'min_time',
  185. ]
  186. for (const column of columns) {
  187. const result = generateQueryPerformanceSql({
  188. preset: 'mostFrequentlyInvoked',
  189. orderBy: { column, order: 'asc' },
  190. })
  191. expect(result.orderBySql).toBe(`ORDER BY ${column} asc`)
  192. }
  193. })
  194. })