From f4409d4ac5f43e82dae2b8302e7e9cf2944822b2 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Thu, 22 Jan 2026 12:26:45 -0700 Subject: [PATCH] fix: updated explain to not crash on non text formats (#42076) updated explain to not crash on non text formats --- .../ExplainVisualizer.utils.ts | 6 ++++ .../UtilityPanel/UtilityTabExplain.tsx | 20 ++++++++++- .../ExplainVisualizer.utils.test.ts | 35 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/ExplainVisualizer/ExplainVisualizer.utils.ts b/apps/studio/components/interfaces/ExplainVisualizer/ExplainVisualizer.utils.ts index 5887d9c02b1..a87fde3c406 100644 --- a/apps/studio/components/interfaces/ExplainVisualizer/ExplainVisualizer.utils.ts +++ b/apps/studio/components/interfaces/ExplainVisualizer/ExplainVisualizer.utils.ts @@ -133,6 +133,12 @@ export function isExplainQuery(rows: readonly unknown[]): boolean { return 'QUERY PLAN' in firstRow && Object.keys(firstRow).length === 1 } +export function isTextFormatExplain(rows: readonly unknown[]): boolean { + if (!isExplainQuery(rows)) return false + const firstRow = rows[0] as Record + return typeof firstRow['QUERY PLAN'] === 'string' +} + export function isExplainSql(sql: string): boolean { return /^\s*explain\b/i.test(sql) } diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityTabExplain.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityTabExplain.tsx index 24849a11086..d1274caf488 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityTabExplain.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityTabExplain.tsx @@ -4,7 +4,10 @@ import { useState } from 'react' import CopyButton from 'components/ui/CopyButton' import { ExplainVisualizer } from 'components/interfaces/ExplainVisualizer/ExplainVisualizer' import { ExplainHeader } from 'components/interfaces/ExplainVisualizer/ExplainVisualizer.Header' -import { isExplainQuery } from 'components/interfaces/ExplainVisualizer/ExplainVisualizer.utils' +import { + isExplainQuery, + isTextFormatExplain, +} from 'components/interfaces/ExplainVisualizer/ExplainVisualizer.utils' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' import Results from './Results' @@ -78,6 +81,7 @@ export function UtilityTabExplain({ id, isExecuting }: UtilityTabExplainProps) { } const isValidExplain = isExplainQuery(explainResult.rows) + const isTextFormat = isTextFormatExplain(explainResult.rows) if (!isValidExplain) { return ( @@ -89,6 +93,20 @@ export function UtilityTabExplain({ id, isExecuting }: UtilityTabExplainProps) { ) } + // Handle non-TEXT formats (JSON, YAML, XML) - show raw output only + if (!isTextFormat) { + return ( +
+
+ + Visual execution plan is only available for TEXT format. Showing raw output. + +
+ +
+ ) + } + const toggleMode = () => setMode(mode === 'visual' ? 'raw' : 'visual') return ( diff --git a/apps/studio/tests/features/explain-visualizer/ExplainVisualizer.utils.test.ts b/apps/studio/tests/features/explain-visualizer/ExplainVisualizer.utils.test.ts index 15aa91be03b..c118e9466e4 100644 --- a/apps/studio/tests/features/explain-visualizer/ExplainVisualizer.utils.test.ts +++ b/apps/studio/tests/features/explain-visualizer/ExplainVisualizer.utils.test.ts @@ -3,6 +3,7 @@ import { splitSqlStatements, isExplainQuery, isExplainSql, + isTextFormatExplain, formatNodeDuration, } from 'components/interfaces/ExplainVisualizer/ExplainVisualizer.utils' @@ -12,6 +13,12 @@ describe('isExplainQuery', () => { expect(isExplainQuery(rows)).toBe(true) }) + test('returns true for JSON format EXPLAIN result rows', () => { + // JSON format returns an array/object in the QUERY PLAN column + const rows = [{ 'QUERY PLAN': [{ Plan: { 'Node Type': 'Seq Scan' } }] }] + expect(isExplainQuery(rows)).toBe(true) + }) + test('returns false for empty array', () => { expect(isExplainQuery([])).toBe(false) }) @@ -22,6 +29,34 @@ describe('isExplainQuery', () => { }) }) +describe('isTextFormatExplain', () => { + test('returns true for TEXT format EXPLAIN result rows', () => { + const rows = [ + { 'QUERY PLAN': 'Seq Scan on users (cost=0.00..10.50 rows=100 width=36)' }, + { 'QUERY PLAN': ' Filter: (active = true)' }, + ] + expect(isTextFormatExplain(rows)).toBe(true) + }) + + test('returns false for JSON format EXPLAIN result rows', () => { + const rows = [ + { 'QUERY PLAN': [{ Plan: { 'Node Type': 'Seq Scan', 'Relation Name': 'users' } }] }, + ] + expect(isTextFormatExplain(rows)).toBe(false) + }) + + test('returns true for YAML format EXPLAIN result rows (returned as string)', () => { + const rows = [ + { + 'QUERY PLAN': + '- 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', + }, + ] + + expect(isTextFormatExplain(rows)).toBe(true) + }) +}) + describe('isExplainSql', () => { test('returns true for EXPLAIN queries', () => { expect(isExplainSql('EXPLAIN SELECT * FROM users')).toBe(true)