ExplainVisualizer.utils.test.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import { describe, expect, test } from 'vitest'
  2. import {
  3. formatNodeDuration,
  4. isExplainQuery,
  5. isExplainSql,
  6. isTextFormatExplain,
  7. splitSqlStatements,
  8. } from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.utils'
  9. describe('isExplainQuery', () => {
  10. test('returns true for valid EXPLAIN result rows', () => {
  11. const rows = [{ 'QUERY PLAN': 'Seq Scan on users' }]
  12. expect(isExplainQuery(rows)).toBe(true)
  13. })
  14. test('returns true for JSON format EXPLAIN result rows', () => {
  15. // JSON format returns an array/object in the QUERY PLAN column
  16. const rows = [{ 'QUERY PLAN': [{ Plan: { 'Node Type': 'Seq Scan' } }] }]
  17. expect(isExplainQuery(rows)).toBe(true)
  18. })
  19. test('returns false for empty array', () => {
  20. expect(isExplainQuery([])).toBe(false)
  21. })
  22. test('returns false for regular query results', () => {
  23. const rows = [{ id: 1, name: 'John' }]
  24. expect(isExplainQuery(rows)).toBe(false)
  25. })
  26. })
  27. describe('isTextFormatExplain', () => {
  28. test('returns true for TEXT format EXPLAIN result rows', () => {
  29. const rows = [
  30. { 'QUERY PLAN': 'Seq Scan on users (cost=0.00..10.50 rows=100 width=36)' },
  31. { 'QUERY PLAN': ' Filter: (active = true)' },
  32. ]
  33. expect(isTextFormatExplain(rows)).toBe(true)
  34. })
  35. test('returns false for JSON format EXPLAIN result rows', () => {
  36. const rows = [
  37. { 'QUERY PLAN': [{ Plan: { 'Node Type': 'Seq Scan', 'Relation Name': 'users' } }] },
  38. ]
  39. expect(isTextFormatExplain(rows)).toBe(false)
  40. })
  41. test('returns true for YAML format EXPLAIN result rows (returned as string)', () => {
  42. const rows = [
  43. {
  44. 'QUERY PLAN':
  45. '- Plan: \n Node Type: "Seq Scan"\n Parallel Aware: false\n Async Capable: false\n Relation Name: "orders"\n Alias: "orders"\n Startup Cost: 0.00\n Total Cost: 97.00\n Plan Rows: 5000\n Plan Width: 41',
  46. },
  47. ]
  48. expect(isTextFormatExplain(rows)).toBe(true)
  49. })
  50. })
  51. describe('isExplainSql', () => {
  52. test('returns true for EXPLAIN queries', () => {
  53. expect(isExplainSql('EXPLAIN SELECT * FROM users')).toBe(true)
  54. expect(isExplainSql('explain analyze SELECT * FROM users')).toBe(true)
  55. expect(isExplainSql(' EXPLAIN SELECT 1')).toBe(true)
  56. })
  57. test('returns false for non-EXPLAIN queries', () => {
  58. expect(isExplainSql('SELECT * FROM users')).toBe(false)
  59. expect(isExplainSql('INSERT INTO users VALUES (1)')).toBe(false)
  60. })
  61. })
  62. describe('formatNodeDuration', () => {
  63. test('returns "-" for undefined', () => {
  64. expect(formatNodeDuration(undefined)).toBe('-')
  65. })
  66. test('formats seconds for large values', () => {
  67. expect(formatNodeDuration(1500)).toBe('1.50s')
  68. })
  69. test('formats milliseconds for medium values', () => {
  70. expect(formatNodeDuration(25.5)).toBe('25.50ms')
  71. })
  72. test('formats microseconds for small values', () => {
  73. expect(formatNodeDuration(0.0005)).toBe('0.5µs')
  74. })
  75. })
  76. describe('splitSqlStatements', () => {
  77. test('splits multiple statements by semicolon', () => {
  78. const sql = 'SELECT * FROM users; SELECT * FROM orders;'
  79. const result = splitSqlStatements(sql)
  80. expect(result).toHaveLength(2)
  81. expect(result[0]).toBe('SELECT * FROM users')
  82. expect(result[1]).toBe('SELECT * FROM orders')
  83. })
  84. test('handles single statement', () => {
  85. const sql = 'SELECT * FROM users'
  86. const result = splitSqlStatements(sql)
  87. expect(result).toHaveLength(1)
  88. expect(result[0]).toBe('SELECT * FROM users')
  89. })
  90. test('ignores semicolons inside single quotes', () => {
  91. const sql = "SELECT * FROM users WHERE name = 'foo;bar'; SELECT 1"
  92. const result = splitSqlStatements(sql)
  93. expect(result).toHaveLength(2)
  94. expect(result[0]).toBe("SELECT * FROM users WHERE name = 'foo;bar'")
  95. })
  96. test('ignores semicolons inside dollar quotes', () => {
  97. const sql = 'SELECT $$ text with ; semicolon $$; SELECT 1'
  98. const result = splitSqlStatements(sql)
  99. expect(result).toHaveLength(2)
  100. expect(result[0]).toBe('SELECT $$ text with ; semicolon $$')
  101. })
  102. test('returns empty array for empty input', () => {
  103. expect(splitSqlStatements('')).toHaveLength(0)
  104. expect(splitSqlStatements(' ')).toHaveLength(0)
  105. })
  106. test('ignores semicolons inside line comments', () => {
  107. const sql = 'SELECT * FROM users -- this is a comment; with semicolon'
  108. const result = splitSqlStatements(sql)
  109. expect(result).toHaveLength(1)
  110. expect(result[0]).toBe('SELECT * FROM users -- this is a comment; with semicolon')
  111. })
  112. test('treats semicolons after line comments as separators', () => {
  113. const sql = 'SELECT * FROM users -- comment\n; SELECT * FROM orders'
  114. const result = splitSqlStatements(sql)
  115. expect(result).toHaveLength(2)
  116. expect(result[0]).toBe('SELECT * FROM users -- comment')
  117. expect(result[1]).toBe('SELECT * FROM orders')
  118. })
  119. test('ignores semicolons inside block comments', () => {
  120. const sql = `SELECT * FROM users /* single-line; comment */ WHERE id = 1;
  121. SELECT * FROM orders
  122. /* multi-line comment
  123. with semicolon; inside
  124. multiple lines */
  125. WHERE status = 'active'`
  126. const result = splitSqlStatements(sql)
  127. expect(result).toHaveLength(2)
  128. expect(result[0]).toBe('SELECT * FROM users /* single-line; comment */ WHERE id = 1')
  129. expect(result[1]).toContain('/* multi-line comment')
  130. expect(result[1]).toContain('with semicolon; inside')
  131. })
  132. test('handles mixed line comments and real semicolons', () => {
  133. const sql = `SELECT * FROM users -- first query; fake semicolon
  134. ; SELECT * FROM orders -- another comment; fake
  135. WHERE status = 'active';`
  136. const result = splitSqlStatements(sql)
  137. expect(result).toHaveLength(2)
  138. expect(result[0]).toContain('SELECT * FROM users')
  139. expect(result[0]).toContain('-- first query; fake semicolon')
  140. expect(result[1]).toContain('SELECT * FROM orders')
  141. expect(result[1]).toContain("WHERE status = 'active'")
  142. })
  143. test('handles mixed block comments and real semicolons', () => {
  144. const sql = 'SELECT 1; /* comment; with semicolon */ SELECT 2;'
  145. const result = splitSqlStatements(sql)
  146. expect(result).toHaveLength(2)
  147. expect(result[0]).toBe('SELECT 1')
  148. expect(result[1]).toBe('/* comment; with semicolon */ SELECT 2')
  149. })
  150. test('handles multiple consecutive line comments', () => {
  151. const sql = `-- Comment 1; with semicolon
  152. -- Comment 2; another semicolon
  153. SELECT * FROM users`
  154. const result = splitSqlStatements(sql)
  155. expect(result).toHaveLength(1)
  156. expect(result[0]).toContain('-- Comment 1; with semicolon')
  157. expect(result[0]).toContain('-- Comment 2; another semicolon')
  158. expect(result[0]).toContain('SELECT * FROM users')
  159. })
  160. test('handles comments at end of statement', () => {
  161. const sql = `SELECT * FROM users; -- end comment; with semicolon
  162. SELECT * FROM orders /* block comment; */`
  163. const result = splitSqlStatements(sql)
  164. expect(result).toHaveLength(2)
  165. expect(result[0]).toBe('SELECT * FROM users')
  166. expect(result[1]).toContain('SELECT * FROM orders')
  167. expect(result[1]).toContain('/* block comment; */')
  168. })
  169. test('handles complex mix of strings, comments, and semicolons', () => {
  170. const sql = `-- Initial comment; with semicolon
  171. SELECT 'string; with semicolon' FROM users /* block; comment */;
  172. -- Next query
  173. SELECT "quoted; identifier" FROM orders -- line; comment
  174. WHERE data = $$ dollar; quote $$;`
  175. const result = splitSqlStatements(sql)
  176. expect(result).toHaveLength(2)
  177. expect(result[0]).toContain("SELECT 'string; with semicolon' FROM users")
  178. expect(result[0]).toContain('/* block; comment */')
  179. expect(result[1]).toContain('SELECT "quoted; identifier" FROM orders')
  180. expect(result[1]).toContain('$$ dollar; quote $$')
  181. })
  182. test('handles statement that is only a comment', () => {
  183. const sql = '-- Just a comment; with semicolon'
  184. const result = splitSqlStatements(sql)
  185. expect(result).toHaveLength(1)
  186. expect(result[0]).toBe('-- Just a comment; with semicolon')
  187. })
  188. test('handles empty statements between semicolons', () => {
  189. const sql = 'SELECT 1;; SELECT 2'
  190. const result = splitSqlStatements(sql)
  191. // Empty statements are filtered out by trim()
  192. expect(result).toHaveLength(2)
  193. expect(result[0]).toBe('SELECT 1')
  194. expect(result[1]).toBe('SELECT 2')
  195. })
  196. test('handles EXPLAIN queries with comments (single statement verification)', () => {
  197. const sql = `-- Query to analyze; note the semicolon
  198. EXPLAIN ANALYZE
  199. SELECT * FROM users
  200. WHERE created_at > NOW() - INTERVAL '1 day' -- last 24 hours; active users`
  201. const result = splitSqlStatements(sql)
  202. // EXPLAIN only works with single statements - verify comments don't cause false splits
  203. expect(result).toHaveLength(1)
  204. expect(result[0]).toContain('EXPLAIN ANALYZE')
  205. })
  206. })