import { describe, expect, test } from 'vitest' import { calculateMaxCost, calculateSummary, parseExplainOutput, parseNodeDetails, } from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.parser' import type { ExplainNode, QueryPlanRow, } from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.types' // Helper to create QueryPlanRow array from strings const toQueryPlanRows = (lines: string[]): QueryPlanRow[] => lines.map((line) => ({ 'QUERY PLAN': line })) describe('parseExplainOutput', () => { describe('simple operations', () => { test('parses a simple Seq Scan', () => { const input = toQueryPlanRows(['Seq Scan on users (cost=0.00..10.50 rows=100 width=36)']) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Seq Scan') expect(result[0].details).toBe('on users') expect(result[0].cost).toEqual({ start: 0, end: 10.5 }) expect(result[0].rows).toBe(100) expect(result[0].width).toBe(36) expect(result[0].actualTime).toBeUndefined() expect(result[0].actualRows).toBeUndefined() expect(result[0].level).toBe(0) expect(result[0].children).toHaveLength(0) }) test('parses an Index Scan', () => { const input = toQueryPlanRows([ 'Index Scan using users_pkey on users (cost=0.29..8.30 rows=1 width=48)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) // Parser keeps "using indexname" as part of operation when "on tablename" is present expect(result[0].operation).toBe('Index Scan using users_pkey') expect(result[0].details).toBe('on users') expect(result[0].cost).toEqual({ start: 0.29, end: 8.3 }) expect(result[0].rows).toBe(1) expect(result[0].width).toBe(48) }) test('parses an Index Only Scan', () => { const input = toQueryPlanRows([ 'Index Only Scan using idx_users_email on users (cost=0.15..4.17 rows=1 width=32)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) // Parser keeps "using indexname" as part of operation when "on tablename" is present expect(result[0].operation).toBe('Index Only Scan using idx_users_email') expect(result[0].details).toBe('on users') }) test('parses Bitmap Index Scan and Bitmap Heap Scan', () => { const input = toQueryPlanRows([ 'Bitmap Heap Scan on users (cost=4.18..13.65 rows=3 width=36)', ' -> Bitmap Index Scan on idx_users_status (cost=0.00..4.18 rows=3 width=0)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Bitmap Heap Scan') expect(result[0].children).toHaveLength(1) expect(result[0].children[0].operation).toBe('Bitmap Index Scan') expect(result[0].children[0].details).toBe('on idx_users_status') }) }) describe('EXPLAIN ANALYZE output', () => { test('parses actual time and actual rows from EXPLAIN ANALYZE', () => { const input = toQueryPlanRows([ 'Seq Scan on users (cost=0.00..10.50 rows=100 width=36) (actual time=0.015..0.123 rows=85 loops=1)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].cost).toEqual({ start: 0, end: 10.5 }) expect(result[0].rows).toBe(100) // estimated rows expect(result[0].actualTime).toEqual({ start: 0.015, end: 0.123 }) expect(result[0].actualRows).toBe(85) // actual rows }) test('skips Planning Time and Execution Time lines', () => { const input = toQueryPlanRows([ 'Seq Scan on users (cost=0.00..10.50 rows=100 width=36) (actual time=0.015..0.123 rows=85 loops=1)', 'Planning Time: 0.089 ms', 'Execution Time: 0.156 ms', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Seq Scan') }) }) describe('nested operations', () => { test('parses nested Hash Join with children', () => { const input = toQueryPlanRows([ 'Hash Join (cost=10.50..25.30 rows=50 width=72)', ' Hash Cond: (orders.user_id = users.id)', ' -> Seq Scan on orders (cost=0.00..12.00 rows=200 width=36)', ' -> Hash (cost=10.50..10.50 rows=100 width=36)', ' -> Seq Scan on users (cost=0.00..10.50 rows=100 width=36)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Hash Join') expect(result[0].details).toContain('Hash Cond:') expect(result[0].children).toHaveLength(2) expect(result[0].children[0].operation).toBe('Seq Scan') expect(result[0].children[0].details).toBe('on orders') expect(result[0].children[1].operation).toBe('Hash') expect(result[0].children[1].children).toHaveLength(1) expect(result[0].children[1].children[0].operation).toBe('Seq Scan') }) test('parses Merge Join', () => { const input = toQueryPlanRows([ 'Merge Join (cost=200.00..350.00 rows=1000 width=80)', ' Merge Cond: (a.id = b.a_id)', ' -> Index Scan using a_pkey on a (cost=0.29..50.00 rows=500 width=40)', ' -> Sort (cost=150.00..155.00 rows=2000 width=40)', ' Sort Key: b.a_id', ' -> Seq Scan on b (cost=0.00..30.00 rows=2000 width=40)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Merge Join') expect(result[0].children).toHaveLength(2) expect(result[0].children[1].operation).toBe('Sort') expect(result[0].children[1].details).toContain('Sort Key:') }) test('parses Nested Loop', () => { const input = toQueryPlanRows([ 'Nested Loop (cost=0.29..16.60 rows=1 width=72)', ' -> Index Scan using users_pkey on users (cost=0.29..8.30 rows=1 width=36)', ' -> Index Scan using orders_user_id_idx on orders (cost=0.00..8.28 rows=1 width=36)', ' Index Cond: (user_id = users.id)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Nested Loop') expect(result[0].children).toHaveLength(2) expect(result[0].children[1].details).toContain('Index Cond:') }) }) describe('aggregate operations', () => { test('parses Aggregate operation', () => { const input = toQueryPlanRows([ 'Aggregate (cost=12.50..12.51 rows=1 width=8)', ' -> Seq Scan on users (cost=0.00..10.50 rows=100 width=0)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Aggregate') expect(result[0].children).toHaveLength(1) }) test('parses HashAggregate with Group Key', () => { const input = toQueryPlanRows([ 'HashAggregate (cost=15.00..17.00 rows=10 width=12)', ' Group Key: status', ' -> Seq Scan on orders (cost=0.00..12.00 rows=200 width=4)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('HashAggregate') expect(result[0].details).toContain('Group Key: status') expect(result[0].children).toHaveLength(1) }) test('parses GroupAggregate with Sort', () => { const input = toQueryPlanRows([ 'GroupAggregate (cost=20.00..25.00 rows=10 width=12)', ' Group Key: category', ' -> Sort (cost=18.00..19.00 rows=200 width=8)', ' Sort Key: category', ' -> Seq Scan on products (cost=0.00..15.00 rows=200 width=8)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('GroupAggregate') expect(result[0].children).toHaveLength(1) expect(result[0].children[0].operation).toBe('Sort') expect(result[0].children[0].children).toHaveLength(1) }) }) describe('sorting and limiting', () => { test('parses Sort operation with Sort Key', () => { const input = toQueryPlanRows([ 'Sort (cost=25.00..27.50 rows=100 width=36)', ' Sort Key: created_at DESC', ' -> Seq Scan on events (cost=0.00..20.00 rows=100 width=36)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Sort') expect(result[0].details).toContain('Sort Key: created_at DESC') expect(result[0].children).toHaveLength(1) }) test('parses Sort with Sort Method in EXPLAIN ANALYZE', () => { const input = toQueryPlanRows([ 'Sort (cost=25.00..27.50 rows=100 width=36) (actual time=0.100..0.150 rows=100 loops=1)', ' Sort Key: created_at DESC', ' Sort Method: quicksort Memory: 32kB', ' -> Seq Scan on events (cost=0.00..20.00 rows=100 width=36) (actual time=0.010..0.050 rows=100 loops=1)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].details).toContain('Sort Key: created_at DESC') expect(result[0].details).toContain('Sort Method: quicksort Memory: 32kB') }) test('parses Limit operation', () => { const input = toQueryPlanRows([ 'Limit (cost=0.00..1.05 rows=10 width=36)', ' -> Seq Scan on users (cost=0.00..10.50 rows=100 width=36)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Limit') expect(result[0].children).toHaveLength(1) }) }) describe('filter conditions', () => { test('parses Filter detail', () => { const input = toQueryPlanRows([ 'Seq Scan on users (cost=0.00..10.50 rows=50 width=36)', " Filter: (status = 'active'::text)", ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].details).toContain("Filter: (status = 'active'::text)") }) test('parses Rows Removed by Filter', () => { const input = toQueryPlanRows([ 'Seq Scan on users (cost=0.00..10.50 rows=50 width=36) (actual time=0.010..0.100 rows=50 loops=1)', " Filter: (status = 'active'::text)", ' Rows Removed by Filter: 50', ]) const result = parseExplainOutput(input) parseNodeDetails(result[0]) expect(result[0].details).toContain('Rows Removed by Filter: 50') expect(result[0].rowsRemovedByFilter).toBe(50) }) test('parses Index Cond', () => { const input = toQueryPlanRows([ 'Index Scan using users_pkey on users (cost=0.29..8.30 rows=1 width=48)', ' Index Cond: (id = 123)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].details).toContain('Index Cond: (id = 123)') }) test('parses Recheck Cond for Bitmap scans', () => { const input = toQueryPlanRows([ 'Bitmap Heap Scan on users (cost=4.18..13.65 rows=3 width=36)', " Recheck Cond: (status = 'active'::text)", ' -> Bitmap Index Scan on idx_users_status (cost=0.00..4.18 rows=3 width=0)', " Index Cond: (status = 'active'::text)", ]) const result = parseExplainOutput(input) expect(result[0].details).toContain("Recheck Cond: (status = 'active'::text)") expect(result[0].children[0].details).toContain("Index Cond: (status = 'active'::text)") }) }) describe('subplans and CTEs', () => { test('parses CTE Scan', () => { const input = toQueryPlanRows([ 'CTE Scan on recent_users (cost=10.50..12.50 rows=100 width=36)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('CTE Scan') expect(result[0].details).toBe('on recent_users') }) test('parses SubPlan reference', () => { const input = toQueryPlanRows([ 'Seq Scan on orders (cost=0.00..25.00 rows=100 width=36)', ' Filter: (total > (SubPlan 1))', ' SubPlan 1', ' -> Aggregate (cost=10.50..10.51 rows=1 width=8)', ' -> Seq Scan on orders orders_1 (cost=0.00..10.50 rows=100 width=4)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].details).toContain('Filter: (total > (SubPlan 1))') expect(result[0].details).toContain('SubPlan 1') }) test('parses InitPlan', () => { const input = toQueryPlanRows([ 'Result (cost=10.51..10.52 rows=1 width=8)', ' InitPlan 1 (returns $0)', ' -> Aggregate (cost=10.50..10.51 rows=1 width=8)', ' -> Seq Scan on users (cost=0.00..10.50 rows=100 width=0)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Result') expect(result[0].details).toContain('InitPlan 1 (returns $0)') }) }) describe('set operations', () => { test('parses Append for UNION ALL', () => { const input = toQueryPlanRows([ 'Append (cost=0.00..21.00 rows=200 width=36)', ' -> Seq Scan on users_2023 (cost=0.00..10.50 rows=100 width=36)', ' -> Seq Scan on users_2024 (cost=0.00..10.50 rows=100 width=36)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Append') expect(result[0].children).toHaveLength(2) }) test('parses Unique for UNION (distinct)', () => { const input = toQueryPlanRows([ 'Unique (cost=25.00..30.00 rows=150 width=36)', ' -> Sort (cost=25.00..26.00 rows=200 width=36)', ' Sort Key: id', ' -> Append (cost=0.00..21.00 rows=200 width=36)', ' -> Seq Scan on users_2023 (cost=0.00..10.50 rows=100 width=36)', ' -> Seq Scan on users_2024 (cost=0.00..10.50 rows=100 width=36)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Unique') expect(result[0].children).toHaveLength(1) expect(result[0].children[0].operation).toBe('Sort') }) }) describe('parallel queries', () => { test('parses Gather with parallel workers', () => { const input = toQueryPlanRows([ 'Gather (cost=1000.00..15000.00 rows=100000 width=36)', ' Workers Planned: 2', ' -> Parallel Seq Scan on large_table (cost=0.00..14000.00 rows=41667 width=36)', " Filter: (status = 'active'::text)", ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Gather') expect(result[0].children).toHaveLength(1) expect(result[0].children[0].operation).toBe('Parallel Seq Scan') }) test('parses Gather Merge', () => { const input = toQueryPlanRows([ 'Gather Merge (cost=5000.00..10000.00 rows=50000 width=36)', ' Workers Planned: 2', ' -> Sort (cost=4000.00..4125.00 rows=25000 width=36)', ' Sort Key: created_at DESC', ' -> Parallel Seq Scan on events (cost=0.00..3000.00 rows=25000 width=36)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Gather Merge') }) }) describe('buffer information', () => { test('parses Buffers information in EXPLAIN (ANALYZE, BUFFERS)', () => { const input = toQueryPlanRows([ 'Seq Scan on users (cost=0.00..10.50 rows=100 width=36) (actual time=0.010..0.100 rows=100 loops=1)', ' Buffers: shared hit=5', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].details).toContain('Buffers: shared hit=5') }) }) describe('edge cases', () => { test('handles empty input', () => { const result = parseExplainOutput([]) expect(result).toHaveLength(0) }) test('handles input with only empty strings', () => { const input = toQueryPlanRows(['', ' ', '']) const result = parseExplainOutput(input) expect(result).toHaveLength(0) }) test('handles malformed metric strings gracefully', () => { const input = toQueryPlanRows(['Seq Scan on users (malformed metrics)']) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Seq Scan') expect(result[0].cost).toBeUndefined() }) test('handles operation without metrics', () => { const input = toQueryPlanRows(['Seq Scan on users']) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Seq Scan') expect(result[0].details).toBe('on users') expect(result[0].cost).toBeUndefined() }) test('handles deeply nested query plans', () => { const input = toQueryPlanRows([ 'Limit (cost=100.00..100.10 rows=10 width=36)', ' -> Sort (cost=100.00..102.50 rows=1000 width=36)', ' Sort Key: total DESC', ' -> Hash Join (cost=50.00..80.00 rows=1000 width=36)', ' Hash Cond: (o.user_id = u.id)', ' -> Seq Scan on orders o (cost=0.00..20.00 rows=1000 width=20)', ' -> Hash (cost=40.00..40.00 rows=500 width=16)', ' -> Seq Scan on users u (cost=0.00..40.00 rows=500 width=16)', ' Filter: (active = true)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Limit') expect(result[0].children[0].operation).toBe('Sort') expect(result[0].children[0].children[0].operation).toBe('Hash Join') expect(result[0].children[0].children[0].children).toHaveLength(2) expect(result[0].children[0].children[0].children[1].children[0].operation).toBe('Seq Scan') }) test('handles One-Time Filter', () => { const input = toQueryPlanRows([ 'Result (cost=0.00..0.01 rows=1 width=0)', ' One-Time Filter: false', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].details).toContain('One-Time Filter: false') }) test('handles Output detail line', () => { const input = toQueryPlanRows([ 'Seq Scan on users (cost=0.00..10.50 rows=100 width=36)', ' Output: id, name, email', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].details).toContain('Output: id, name, email') }) test('handles invalid cost values gracefully', () => { const input = toQueryPlanRows([ 'Seq Scan on users (cost=invalid..notanumber rows=100 width=36)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Seq Scan') // Should not parse invalid cost at all expect(result[0].cost).toBeUndefined() }) test('handles invalid rows value gracefully', () => { const input = toQueryPlanRows([ 'Seq Scan on users (cost=0.00..10.50 rows=notanumber width=36)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Seq Scan') // Should not parse invalid rows at all (regex won't match) expect(result[0].rows).toBeUndefined() }) test('handles invalid actual time values gracefully', () => { const input = toQueryPlanRows([ 'Seq Scan on users (cost=0.00..10.50 rows=100 width=36) (actual time=invalid..notanumber rows=85 loops=1)', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) // Should not parse invalid actual time at all expect(result[0].actualTime).toBeUndefined() }) test('handles invalid rowsRemovedByFilter value gracefully', () => { const input = toQueryPlanRows([ 'Seq Scan on users (cost=0.00..10.50 rows=100 width=36)', " Filter: (status = 'active')", ' Rows Removed by Filter: notanumber', ]) const result = parseExplainOutput(input) parseNodeDetails(result[0]) expect(result).toHaveLength(1) // Should not parse invalid value at all (regex won't match) expect(result[0].rowsRemovedByFilter).toBeUndefined() }) test('parsed numeric fields are finite for valid input', () => { const input = toQueryPlanRows([ 'Seq Scan on users (cost=0.00..10.50 rows=100 width=36) (actual time=0.015..0.123 rows=85 loops=1)', " Filter: (status = 'active')", ' Rows Removed by Filter: 15', ]) const result = parseExplainOutput(input) parseNodeDetails(result[0]) expect(result).toHaveLength(1) const node = result[0] // Verify all numeric values are finite if (node.cost) { expect(Number.isFinite(node.cost.start)).toBe(true) expect(Number.isFinite(node.cost.end)).toBe(true) } if (node.actualTime) { expect(Number.isFinite(node.actualTime.start)).toBe(true) expect(Number.isFinite(node.actualTime.end)).toBe(true) } if (node.rows !== undefined) { expect(Number.isFinite(node.rows)).toBe(true) } if (node.actualRows !== undefined) { expect(Number.isFinite(node.actualRows)).toBe(true) } if (node.width !== undefined) { expect(Number.isFinite(node.width)).toBe(true) } if (node.rowsRemovedByFilter !== undefined) { expect(Number.isFinite(node.rowsRemovedByFilter)).toBe(true) } }) }) describe('complex real-world queries', () => { test('parses a complex analytical query', () => { const input = toQueryPlanRows([ 'Limit (cost=1500.00..1500.05 rows=20 width=48) (actual time=15.234..15.240 rows=20 loops=1)', ' -> Sort (cost=1500.00..1525.00 rows=10000 width=48) (actual time=15.232..15.235 rows=20 loops=1)', ' Sort Key: (sum(o.total)) DESC', ' Sort Method: top-N heapsort Memory: 27kB', ' -> HashAggregate (cost=1200.00..1300.00 rows=10000 width=48) (actual time=12.456..14.789 rows=8543 loops=1)', ' Group Key: u.id', ' Batches: 1 Memory Usage: 1169kB', ' -> Hash Join (cost=125.00..950.00 rows=50000 width=20) (actual time=1.234..8.567 rows=50000 loops=1)', ' Hash Cond: (o.user_id = u.id)', ' -> Seq Scan on orders o (cost=0.00..750.00 rows=50000 width=12) (actual time=0.012..3.456 rows=50000 loops=1)', ' -> Hash (cost=100.00..100.00 rows=2000 width=8) (actual time=1.111..1.111 rows=2000 loops=1)', ' Buckets: 2048 Batches: 1 Memory Usage: 95kB', ' -> Seq Scan on users u (cost=0.00..100.00 rows=2000 width=8) (actual time=0.008..0.567 rows=2000 loops=1)', ' Filter: (active = true)', ' Rows Removed by Filter: 500', ]) const result = parseExplainOutput(input) expect(result).toHaveLength(1) expect(result[0].operation).toBe('Limit') expect(result[0].actualTime).toEqual({ start: 15.234, end: 15.24 }) expect(result[0].actualRows).toBe(20) // Navigate to the deepest Seq Scan on users const hashJoin = result[0].children[0].children[0].children[0] expect(hashJoin.operation).toBe('Hash Join') const hash = hashJoin.children[1] expect(hash.operation).toBe('Hash') const usersScan = hash.children[0] expect(usersScan.operation).toBe('Seq Scan') expect(usersScan.details).toContain('Filter: (active = true)') expect(usersScan.details).toContain('Rows Removed by Filter: 500') }) }) }) describe('parseNodeDetails', () => { test('parses Rows Removed by Filter from node details', () => { const node: ExplainNode = { operation: 'Seq Scan', details: "on users\nFilter: (status = 'active')\nRows Removed by Filter: 150", cost: { start: 0, end: 10.5 }, rows: 100, width: 36, level: 0, children: [], raw: '', } parseNodeDetails(node) expect(node.rowsRemovedByFilter).toBe(150) }) test('handles node without Rows Removed by Filter', () => { const node: ExplainNode = { operation: 'Seq Scan', details: 'on users', cost: { start: 0, end: 10.5 }, rows: 100, width: 36, level: 0, children: [], raw: '', } parseNodeDetails(node) expect(node.rowsRemovedByFilter).toBeUndefined() }) test('recursively parses details for children', () => { const node: ExplainNode = { operation: 'Hash Join', details: '', cost: { start: 0, end: 50 }, rows: 100, width: 72, level: 0, children: [ { operation: 'Seq Scan', details: 'on orders\nRows Removed by Filter: 200', cost: { start: 0, end: 20 }, rows: 50, width: 36, level: 1, children: [], raw: '', }, { operation: 'Seq Scan', details: 'on users\nRows Removed by Filter: 75', cost: { start: 0, end: 15 }, rows: 25, width: 36, level: 1, children: [], raw: '', }, ], raw: '', } parseNodeDetails(node) expect(node.children[0].rowsRemovedByFilter).toBe(200) expect(node.children[1].rowsRemovedByFilter).toBe(75) }) }) describe('calculateMaxCost', () => { test('returns 0 for empty tree', () => { const result = calculateMaxCost([]) expect(result).toBe(0) }) test('returns cost.end for single node', () => { const tree: ExplainNode[] = [ { operation: 'Seq Scan', details: 'on users', cost: { start: 0, end: 25.5 }, rows: 100, width: 36, level: 0, children: [], raw: '', }, ] const result = calculateMaxCost(tree) expect(result).toBe(25.5) }) test('prefers cost.end over actualTime.end (uses actualTime as fallback)', () => { // When both cost and actualTime are present, cost takes precedence const treeWithBoth: ExplainNode[] = [ { operation: 'Seq Scan', details: 'on users', cost: { start: 0, end: 10.5 }, rows: 100, width: 36, actualTime: { start: 0.01, end: 50.123 }, actualRows: 100, level: 0, children: [], raw: '', }, ] expect(calculateMaxCost(treeWithBoth)).toBe(10.5) // When only actualTime is present, it's used as fallback const treeOnlyActualTime: ExplainNode[] = [ { operation: 'Seq Scan', details: 'on users', rows: 100, width: 36, actualTime: { start: 0.01, end: 50.123 }, actualRows: 100, level: 0, children: [], raw: '', }, ] expect(calculateMaxCost(treeOnlyActualTime)).toBe(50.123) }) test('finds maximum across nested children', () => { const tree: ExplainNode[] = [ { operation: 'Limit', details: '', cost: { start: 0, end: 100 }, rows: 10, width: 36, level: 0, children: [ { operation: 'Sort', details: '', cost: { start: 0, end: 250 }, // This is the maximum rows: 1000, width: 36, level: 1, children: [ { operation: 'Seq Scan', details: 'on users', cost: { start: 0, end: 150 }, rows: 1000, width: 36, level: 2, children: [], raw: '', }, ], raw: '', }, ], raw: '', }, ] const result = calculateMaxCost(tree) expect(result).toBe(250) }) test('handles multiple root nodes', () => { const tree: ExplainNode[] = [ { operation: 'Seq Scan', details: 'on users', cost: { start: 0, end: 30 }, rows: 100, width: 36, level: 0, children: [], raw: '', }, { operation: 'Seq Scan', details: 'on orders', cost: { start: 0, end: 75 }, rows: 200, width: 36, level: 0, children: [], raw: '', }, ] const result = calculateMaxCost(tree) expect(result).toBe(75) }) test('handles nodes without cost or actualTime', () => { const tree: ExplainNode[] = [ { operation: 'Result', details: '', level: 0, children: [], raw: '', }, ] const result = calculateMaxCost(tree) expect(result).toBe(0) }) }) describe('calculateSummary', () => { test('returns default values for empty tree', () => { const result = calculateSummary([]) expect(result).toEqual({ totalTime: 0, totalCost: 0, maxCost: 0, hasSeqScan: false, seqScanTables: [], hasIndexScan: false, }) }) test('calculates totalCost from root node cost.end', () => { const tree: ExplainNode[] = [ { operation: 'Seq Scan', details: 'on users', cost: { start: 0, end: 45.5 }, rows: 100, width: 36, level: 0, children: [], raw: '', }, ] const result = calculateSummary(tree) expect(result.totalCost).toBe(45.5) }) test('calculates maxCost from maximum cost across all nodes', () => { const tree: ExplainNode[] = [ { operation: 'Limit', details: '', cost: { start: 0, end: 100 }, rows: 10, width: 36, level: 0, children: [ { operation: 'Sort', details: '', cost: { start: 0, end: 250 }, // This is the maximum rows: 1000, width: 36, level: 1, children: [ { operation: 'Seq Scan', details: 'on users', cost: { start: 0, end: 150 }, rows: 1000, width: 36, level: 2, children: [], raw: '', }, ], raw: '', }, ], raw: '', }, ] const result = calculateSummary(tree) expect(result.totalCost).toBe(100) // Root node cost expect(result.maxCost).toBe(250) // Maximum across all nodes }) test('calculates totalTime from actualTime.end', () => { const tree: ExplainNode[] = [ { operation: 'Seq Scan', details: 'on users', cost: { start: 0, end: 10.5 }, rows: 100, width: 36, actualTime: { start: 0.01, end: 123.456 }, actualRows: 100, level: 0, children: [], raw: '', }, ] const result = calculateSummary(tree) expect(result.totalTime).toBe(123.456) }) test('detects Seq Scan and extracts table name', () => { const tree: ExplainNode[] = [ { operation: 'Seq Scan', details: 'on users', cost: { start: 0, end: 10.5 }, rows: 100, width: 36, level: 0, children: [], raw: '', }, ] const result = calculateSummary(tree) expect(result.hasSeqScan).toBe(true) expect(result.seqScanTables).toEqual(['users']) }) test('detects multiple Seq Scans on different tables', () => { const tree: ExplainNode[] = [ { operation: 'Hash Join', details: '', cost: { start: 0, end: 50 }, rows: 100, width: 72, level: 0, children: [ { operation: 'Seq Scan', details: 'on orders', cost: { start: 0, end: 20 }, rows: 100, width: 36, level: 1, children: [], raw: '', }, { operation: 'Seq Scan', details: 'on users', cost: { start: 0, end: 15 }, rows: 50, width: 36, level: 1, children: [], raw: '', }, ], raw: '', }, ] const result = calculateSummary(tree) expect(result.hasSeqScan).toBe(true) expect(result.seqScanTables).toEqual(['orders', 'users']) }) test('detects Index Scan', () => { const tree: ExplainNode[] = [ { operation: 'Index Scan using users_pkey', details: 'on users', cost: { start: 0.29, end: 8.3 }, rows: 1, width: 48, level: 0, children: [], raw: '', }, ] const result = calculateSummary(tree) expect(result.hasIndexScan).toBe(true) expect(result.hasSeqScan).toBe(false) }) test('detects Index Only Scan', () => { const tree: ExplainNode[] = [ { operation: 'Index Only Scan', details: 'using idx_users_email on users', cost: { start: 0.15, end: 4.17 }, rows: 1, width: 32, level: 0, children: [], raw: '', }, ] const result = calculateSummary(tree) expect(result.hasIndexScan).toBe(true) }) test('detects Bitmap Index Scan', () => { const tree: ExplainNode[] = [ { operation: 'Bitmap Heap Scan', details: 'on users', cost: { start: 4.18, end: 13.65 }, rows: 3, width: 36, level: 0, children: [ { operation: 'Bitmap Index Scan', details: 'on idx_users_status', cost: { start: 0, end: 4.18 }, rows: 3, width: 0, level: 1, children: [], raw: '', }, ], raw: '', }, ] const result = calculateSummary(tree) expect(result.hasIndexScan).toBe(true) }) test('handles complex query with both seq and index scans', () => { const tree: ExplainNode[] = [ { operation: 'Hash Join', details: '', cost: { start: 10.5, end: 35.8 }, rows: 50, width: 72, actualTime: { start: 0.5, end: 2.345 }, actualRows: 48, level: 0, children: [ { operation: 'Seq Scan', details: 'on orders', cost: { start: 0, end: 20 }, rows: 100, width: 36, actualTime: { start: 0.01, end: 0.5 }, actualRows: 95, level: 1, children: [], raw: '', }, { operation: 'Index Scan using users_pkey', details: 'on users', cost: { start: 0.29, end: 8.3 }, rows: 1, width: 36, actualTime: { start: 0.005, end: 0.015 }, actualRows: 1, level: 1, children: [], raw: '', }, ], raw: '', }, ] const result = calculateSummary(tree) expect(result.totalCost).toBe(35.8) // Root node cost expect(result.maxCost).toBe(35.8) // Maximum cost across all nodes (root is highest) expect(result.totalTime).toBe(2.345) expect(result.hasSeqScan).toBe(true) expect(result.hasIndexScan).toBe(true) expect(result.seqScanTables).toEqual(['orders']) }) })