From a70264c8adc92463853635a487edb66ebdd2a774 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Thu, 2 Apr 2026 07:30:00 -0600 Subject: [PATCH] fix(studio): coerce index advisor cost values to numbers (#44397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Coerces `before`/`after` cost values to `Number()` in `QueryPanelScoreSection` and `calculateImprovement` before any comparison or arithmetic - Fixes contradictory index advisor display where correct cost numbers showed 0% improvement and wrong arrow direction ## Root Cause When `index_advisor_result` is prefetched from the Reports SQL query (via `json_build_object`), cost values can arrive as strings instead of numbers. JavaScript string comparison is lexicographic, producing wrong results: | Expression | Numbers | Strings | |---|---|---| | `after > before` (arrow) | `50 > 100` → `false` ✅ | `"50" > "100"` → `true` ❌ | | `costBefore <= costAfter` (improvement calc) | `100 <= 50` → `false` ✅ | `"100" <= "50"` → `true` ❌ | The direct fetch path (`retrieve-index-advisor-result-query.ts`) validates through Zod and is unaffected. Only the prefetched path lacks validation. ## Summary by CodeRabbit * **Bug Fixes** * Improved numeric value handling in query performance calculations to ensure more accurate and reliable improvement metrics. * **Refactor** * Enhanced type safety and numeric coercion for query performance score comparisons, resulting in more consistent and robust metric calculations. --- .../IndexAdvisor/index-advisor.utils.ts | 16 +-- .../QueryPerformance/QueryPanel.tsx | 113 +++++++++--------- 2 files changed, 68 insertions(+), 61 deletions(-) diff --git a/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils.ts b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils.ts index e0b80a3d5ad..723129f64b2 100644 --- a/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils.ts +++ b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils.ts @@ -27,16 +27,18 @@ export function calculateImprovement( costBefore: number | undefined, costAfter: number | undefined ): number { - if ( - costBefore === undefined || - costAfter === undefined || - costBefore <= 0 || - costBefore <= costAfter - ) { + if (costBefore === undefined || costAfter === undefined) { return 0 } - return ((costBefore - costAfter) / costBefore) * 100 + const before = Number(costBefore) + const after = Number(costAfter) + + if (before <= 0 || before <= after) { + return 0 + } + + return ((before - after) / before) * 100 } interface CreateIndexParams { diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPanel.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPanel.tsx index 7c700952fa2..d052c10c2c5 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPanel.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPanel.tsx @@ -21,8 +21,8 @@ export const QueryPanelScoreSection = ({ className, name, description, - before, - after, + before: rawBefore, + after: rawAfter, hideArrowMarkers = false, }: { className?: string @@ -31,58 +31,63 @@ export const QueryPanelScoreSection = ({ before?: number after?: number hideArrowMarkers?: boolean -}) => ( -
-
- {name} - - - - - - {description} - - -
-
-
- Currently: - - {before} - +}) => { + const before = rawBefore !== undefined ? Number(rawBefore) : undefined + const after = rawAfter !== undefined ? Number(rawAfter) : undefined + + return ( +
+
+ {name} + + + + + + {description} + +
- {before !== undefined && after !== undefined && before !== after && ( -
- With index: - {after} - {before !== undefined && !hideArrowMarkers && ( -
- {after > before ? ( - - ) : ( - - )} - {typeof before === 'number' && before !== 0 && !isNaN(before) && isFinite(before) && ( - before ? 'text-warning' : 'text-brand' - )} - > - {(((before - after) / before) * 100).toFixed(2)}% - - )} -
- )} +
+
+ Currently: + + {before} +
- )} + {before !== undefined && after !== undefined && before !== after && ( +
+ With index: + {after} + {before !== undefined && !hideArrowMarkers && ( +
+ {after > before ? ( + + ) : ( + + )} + {before !== 0 && !isNaN(before) && isFinite(before) && ( + before ? 'text-warning' : 'text-brand' + )} + > + {(((before - after) / before) * 100).toFixed(2)}% + + )} +
+ )} +
+ )} +
-
-) + ) +}