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