ExplainVisualizer.utils.test.ts252 lines · main
| 1 | import { describe, expect, test } from 'vitest' |
| 2 | |
| 3 | import { |
| 4 | formatNodeDuration, |
| 5 | isExplainQuery, |
| 6 | isExplainSql, |
| 7 | isTextFormatExplain, |
| 8 | splitSqlStatements, |
| 9 | } from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.utils' |
| 10 | |
| 11 | describe('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 | |
| 33 | describe('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 | |
| 61 | describe('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 | |
| 74 | describe('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 | |
| 92 | describe('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; |
| 150 | SELECT * FROM orders |
| 151 | /* multi-line comment |
| 152 | with semicolon; inside |
| 153 | multiple lines */ |
| 154 | WHERE 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 |
| 166 | WHERE 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 |
| 188 | SELECT * 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 |
| 199 | SELECT * 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 |
| 210 | SELECT 'string; with semicolon' FROM users /* block; comment */; |
| 211 | -- Next query |
| 212 | SELECT "quoted; identifier" FROM orders -- line; comment |
| 213 | WHERE 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 |
| 243 | EXPLAIN ANALYZE |
| 244 | SELECT * FROM users |
| 245 | WHERE 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 | }) |