supamonitor.utils.test.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. import { safeSql } from '@supabase/pg-meta'
  2. import { describe, expect, it } from 'vitest'
  3. import type { ParsedLogEntry } from '../QueryInsights.types'
  4. import {
  5. aggregateLogsByQuery,
  6. parseSupamonitorLogs,
  7. transformLogsToChartData,
  8. } from './supamonitor.utils'
  9. const makeSampleLog = (overrides: Partial<ParsedLogEntry> = {}): any => ({
  10. timestamp: '2025-01-01T00:00:00Z',
  11. application_name: 'test_app',
  12. calls: 10,
  13. database_name: 'test_db',
  14. query: 'SELECT 1',
  15. query_id: 1,
  16. total_exec_time: 100,
  17. total_plan_time: 20,
  18. user_name: 'postgres',
  19. mean_exec_time: 10,
  20. mean_plan_time: 2,
  21. min_exec_time: 1,
  22. max_exec_time: 50,
  23. min_plan_time: 0.5,
  24. max_plan_time: 5,
  25. p50_exec_time: 8,
  26. p95_exec_time: 40,
  27. p50_plan_time: 1.5,
  28. p95_plan_time: 4,
  29. ...overrides,
  30. })
  31. describe('parseSupamonitorLogs', () => {
  32. it('returns empty array for null or empty input', () => {
  33. expect(parseSupamonitorLogs(null as any)).toEqual([])
  34. expect(parseSupamonitorLogs([])).toEqual([])
  35. })
  36. it('parses log entries preserving all fields', () => {
  37. const raw = [makeSampleLog()]
  38. const result = parseSupamonitorLogs(raw)
  39. expect(result).toHaveLength(1)
  40. expect(result[0]).toEqual({
  41. timestamp: '2025-01-01T00:00:00Z',
  42. application_name: 'test_app',
  43. calls: 10,
  44. database_name: 'test_db',
  45. query: 'SELECT 1',
  46. query_id: 1,
  47. total_exec_time: 100,
  48. total_plan_time: 20,
  49. user_name: 'postgres',
  50. mean_exec_time: 10,
  51. mean_plan_time: 2,
  52. min_exec_time: 1,
  53. max_exec_time: 50,
  54. min_plan_time: 0.5,
  55. max_plan_time: 5,
  56. p50_exec_time: 8,
  57. p95_exec_time: 40,
  58. p50_plan_time: 1.5,
  59. p95_plan_time: 4,
  60. })
  61. })
  62. it('handles multiple log entries', () => {
  63. const raw = [makeSampleLog(), makeSampleLog({ query: safeSql`SELECT 2`, query_id: 2 })]
  64. const result = parseSupamonitorLogs(raw)
  65. expect(result).toHaveLength(2)
  66. })
  67. })
  68. describe('transformLogsToChartData', () => {
  69. it('returns empty array for null or empty input', () => {
  70. expect(transformLogsToChartData(null as any)).toEqual([])
  71. expect(transformLogsToChartData([])).toEqual([])
  72. })
  73. it('filters out entries with no timestamp', () => {
  74. const logs: ParsedLogEntry[] = [{ query: safeSql`SELECT 1`, calls: 5 }]
  75. const result = transformLogsToChartData(logs)
  76. expect(result).toEqual([])
  77. })
  78. it('filters out entries with invalid timestamp', () => {
  79. const logs: ParsedLogEntry[] = [{ timestamp: 'not-a-date', calls: 5 }]
  80. const result = transformLogsToChartData(logs)
  81. expect(result).toEqual([])
  82. })
  83. it('transforms a valid log entry into a chart data point', () => {
  84. const logs: ParsedLogEntry[] = [
  85. {
  86. timestamp: '2025-01-01T00:00:00Z',
  87. mean_exec_time: 10,
  88. mean_plan_time: 2,
  89. min_exec_time: 1,
  90. max_exec_time: 50,
  91. min_plan_time: 0.5,
  92. max_plan_time: 5,
  93. p50_exec_time: 8,
  94. p95_exec_time: 40,
  95. p50_plan_time: 1.5,
  96. p95_plan_time: 4,
  97. calls: 10,
  98. },
  99. ]
  100. const result = transformLogsToChartData(logs)
  101. expect(result).toHaveLength(1)
  102. expect(result[0]).toEqual({
  103. period_start: new Date('2025-01-01T00:00:00Z').getTime(),
  104. timestamp: '2025-01-01T00:00:00Z',
  105. query_latency: 12, // 10 + 2
  106. mean_time: 10,
  107. min_time: 1.5, // 1 + 0.5
  108. max_time: 55, // 50 + 5
  109. stddev_time: 0,
  110. p50_time: 9.5, // 8 + 1.5
  111. p95_time: 44, // 40 + 4
  112. rows_read: 0,
  113. calls: 10,
  114. cache_hits: 0,
  115. cache_misses: 0,
  116. })
  117. })
  118. it('defaults missing numeric fields to 0', () => {
  119. const logs: ParsedLogEntry[] = [
  120. {
  121. timestamp: '2025-06-01T12:00:00Z',
  122. },
  123. ]
  124. const result = transformLogsToChartData(logs)
  125. expect(result).toHaveLength(1)
  126. expect(result[0].query_latency).toBe(0)
  127. expect(result[0].calls).toBe(0)
  128. expect(result[0].min_time).toBe(0)
  129. expect(result[0].max_time).toBe(0)
  130. })
  131. it('sorts results by period_start ascending', () => {
  132. const logs: ParsedLogEntry[] = [
  133. { timestamp: '2025-01-03T00:00:00Z', mean_exec_time: 1 },
  134. { timestamp: '2025-01-01T00:00:00Z', mean_exec_time: 2 },
  135. { timestamp: '2025-01-02T00:00:00Z', mean_exec_time: 3 },
  136. ]
  137. const result = transformLogsToChartData(logs)
  138. expect(result).toHaveLength(3)
  139. expect(result[0].timestamp).toBe('2025-01-01T00:00:00Z')
  140. expect(result[1].timestamp).toBe('2025-01-02T00:00:00Z')
  141. expect(result[2].timestamp).toBe('2025-01-03T00:00:00Z')
  142. })
  143. })
  144. describe('aggregateLogsByQuery', () => {
  145. it('returns empty array for null or empty input', () => {
  146. expect(aggregateLogsByQuery(null as any)).toEqual([])
  147. expect(aggregateLogsByQuery([])).toEqual([])
  148. })
  149. it('skips entries with empty or whitespace-only queries', () => {
  150. const logs: ParsedLogEntry[] = [
  151. { query: safeSql``, calls: 5 },
  152. { query: safeSql` `, calls: 3 },
  153. ]
  154. const result = aggregateLogsByQuery(logs)
  155. expect(result).toEqual([])
  156. })
  157. it('aggregates a single log entry correctly', () => {
  158. const logs: ParsedLogEntry[] = [
  159. {
  160. query: safeSql`SELECT 1`,
  161. user_name: 'postgres',
  162. application_name: 'app',
  163. calls: 10,
  164. total_exec_time: 100,
  165. total_plan_time: 20,
  166. min_exec_time: 1,
  167. max_exec_time: 50,
  168. min_plan_time: 0.5,
  169. max_plan_time: 5,
  170. },
  171. ]
  172. const result = aggregateLogsByQuery(logs)
  173. expect(result).toHaveLength(1)
  174. expect(result[0].query).toBe('SELECT 1')
  175. expect(result[0].rolname).toBe('postgres')
  176. expect(result[0].application_name).toBe('app')
  177. expect(result[0].calls).toBe(10)
  178. expect(result[0].total_time).toBe(120)
  179. expect(result[0].mean_time).toBe(12)
  180. expect(result[0].min_time).toBe(1.5)
  181. expect(result[0].max_time).toBe(55)
  182. expect(result[0].prop_total_time).toBe(100)
  183. })
  184. it('aggregates multiple entries for the same query', () => {
  185. const logs: ParsedLogEntry[] = [
  186. {
  187. query: safeSql`SELECT 1`,
  188. user_name: 'postgres',
  189. calls: 5,
  190. total_exec_time: 50,
  191. total_plan_time: 10,
  192. min_exec_time: 2,
  193. max_exec_time: 20,
  194. min_plan_time: 1,
  195. max_plan_time: 3,
  196. },
  197. {
  198. query: safeSql`SELECT 1`,
  199. user_name: 'postgres',
  200. calls: 10,
  201. total_exec_time: 100,
  202. total_plan_time: 20,
  203. min_exec_time: 1,
  204. max_exec_time: 50,
  205. min_plan_time: 0.5,
  206. max_plan_time: 5,
  207. },
  208. ]
  209. const result = aggregateLogsByQuery(logs)
  210. expect(result).toHaveLength(1)
  211. expect(result[0].calls).toBe(15) // 5 + 10
  212. expect(result[0].total_time).toBe(180) // (50+10) + (100+20)
  213. expect(result[0].mean_time).toBe(12) // 180 / 15
  214. expect(result[0].min_time).toBe(1.5) // min(2+1, 1+0.5) = 1.5
  215. expect(result[0].max_time).toBe(55) // max(20+3, 50+5) = 55
  216. expect(result[0]._count).toBe(2) // 2 log entries
  217. })
  218. it('normalizes whitespace differences in queries', () => {
  219. const logs: ParsedLogEntry[] = [
  220. { query: safeSql`SELECT 1`, calls: 5, total_exec_time: 50, total_plan_time: 0 },
  221. { query: safeSql`SELECT 1`, calls: 3, total_exec_time: 30, total_plan_time: 0 },
  222. ]
  223. const result = aggregateLogsByQuery(logs)
  224. expect(result).toHaveLength(1)
  225. expect(result[0].calls).toBe(8)
  226. })
  227. it('sorts results by total_time descending', () => {
  228. const logs: ParsedLogEntry[] = [
  229. { query: safeSql`SELECT 1`, calls: 1, total_exec_time: 10, total_plan_time: 0 },
  230. { query: safeSql`SELECT 2`, calls: 1, total_exec_time: 100, total_plan_time: 0 },
  231. { query: safeSql`SELECT 3`, calls: 1, total_exec_time: 50, total_plan_time: 0 },
  232. ]
  233. const result = aggregateLogsByQuery(logs)
  234. expect(result).toHaveLength(3)
  235. expect(result[0].query).toBe('SELECT 2')
  236. expect(result[1].query).toBe('SELECT 3')
  237. expect(result[2].query).toBe('SELECT 1')
  238. })
  239. it('calculates prop_total_time as percentage of total execution', () => {
  240. const logs: ParsedLogEntry[] = [
  241. { query: safeSql`SELECT 1`, calls: 1, total_exec_time: 75, total_plan_time: 0 },
  242. { query: safeSql`SELECT 2`, calls: 1, total_exec_time: 25, total_plan_time: 0 },
  243. ]
  244. const result = aggregateLogsByQuery(logs)
  245. expect(result[0].prop_total_time).toBe(75)
  246. expect(result[1].prop_total_time).toBe(25)
  247. })
  248. it('handles zero calls gracefully (mean_time defaults to 0)', () => {
  249. const logs: ParsedLogEntry[] = [
  250. { query: safeSql`SELECT 1`, calls: 0, total_exec_time: 100, total_plan_time: 0 },
  251. ]
  252. const result = aggregateLogsByQuery(logs)
  253. expect(result).toHaveLength(1)
  254. expect(result[0].mean_time).toBe(0)
  255. })
  256. it('sets static fields correctly', () => {
  257. const logs: ParsedLogEntry[] = [
  258. { query: safeSql`SELECT 1`, calls: 1, total_exec_time: 10, total_plan_time: 0 },
  259. ]
  260. const result = aggregateLogsByQuery(logs)
  261. expect(result[0].rows_read).toBe(0)
  262. expect(result[0].cache_hit_rate).toBe(0)
  263. expect(result[0].index_advisor_result).toBeNull()
  264. expect(result[0]._total_cache_hits).toBe(0)
  265. expect(result[0]._total_cache_misses).toBe(0)
  266. })
  267. })