@@ -20,6 +20,7 @@ import { QueryResultWebviewController } from "./queryResultWebViewController";
2020import store , { SubKeys } from "./singletonStore" ;
2121import { JsonFormattingEditProvider } from "../utils/jsonFormatter" ;
2222import * as LocalizedConstants from "../constants/locConstants" ;
23+ import { generateGuid } from "../models/utils" ;
2324
2425export const MAX_VIEW_COLUMN = 9 ;
2526
@@ -312,13 +313,34 @@ export function registerCommonRequestHandlers(
312313 return store . get ( message . uri , SubKeys . PaneScrollPosition ) ?? { scrollTop : 0 } ;
313314 } ) ;
314315
315- webviewController . onRequest ( qr . SetSelectionSummaryRequest . type , async ( message ) => {
316+ let lastSummaryRequestId = "" ;
317+ webviewController . onNotification ( qr . SetSelectionSummaryRequest . type , async ( message ) => {
318+ let currentId = generateGuid ( ) ;
319+ lastSummaryRequestId = currentId ;
320+
321+ // Fetch all the data needed for the summary
322+ const summaryData = await webviewViewController
323+ . getSqlOutputContentProvider ( )
324+ . generateSelectionSummaryData (
325+ message . uri ,
326+ message . batchId ,
327+ message . resultId ,
328+ message . selection ,
329+ ) ;
330+
316331 const controller =
317332 webviewController instanceof QueryResultWebviewPanelController
318333 ? webviewController . getQueryResultWebviewViewController ( )
319334 : webviewController ;
320335
321- controller . updateSelectionSummaryStatusItem ( message . summary ) ;
336+ /**
337+ * Only update the summary if this is the latest request. This ensures that if multiple
338+ * requests are made in quick succession, we don't overwrite the summary with stale data.
339+ */
340+ if ( lastSummaryRequestId !== currentId ) {
341+ return ;
342+ }
343+ controller . updateSelectionSummaryStatusItem ( summaryData ) ;
322344 } ) ;
323345
324346 webviewController . registerReducer ( "setResultTab" , async ( state , payload ) => {
@@ -414,3 +436,182 @@ export function countResultSets(
414436 }
415437 return count ;
416438}
439+
440+ /**
441+ * Calculate selection summary statistics for grid selections
442+ * @param selections Array of grid selection ranges
443+ * @param grid Mock grid object with getCellNode method
444+ * @param isSelection Whether this is an actual selection (true) or clearing stats (false)
445+ * @returns Promise<SelectionSummaryStats> Summary statistics
446+ */
447+ export async function selectionSummaryHelper (
448+ selections : qr . ISlickRange [ ] ,
449+ grid : {
450+ getCellNode : ( row : number , col : number ) => HTMLElement | undefined ;
451+ getColumns : ( ) => any [ ] ;
452+ } ,
453+ isSelection : boolean ,
454+ ) : Promise < qr . SelectionSummaryStats > {
455+ const summary : qr . SelectionSummaryStats = {
456+ count : - 1 ,
457+ average : "" ,
458+ sum : 0 ,
459+ min : 0 ,
460+ max : 0 ,
461+ removeSelectionStats : ! isSelection ,
462+ distinctCount : - 1 ,
463+ nullCount : - 1 ,
464+ } ;
465+
466+ if ( ! isSelection ) {
467+ return summary ;
468+ }
469+
470+ const columns = grid . getColumns ( ) ;
471+ if ( ! columns || columns . length === 0 ) {
472+ return summary ;
473+ }
474+
475+ if ( ! selections || selections . length === 0 ) {
476+ return summary ;
477+ }
478+
479+ // Reset values for actual calculation
480+ summary . count = 0 ;
481+ summary . distinctCount = 0 ;
482+ summary . nullCount = 0 ;
483+ summary . min = Infinity ;
484+ summary . max = - Infinity ;
485+ summary . removeSelectionStats = false ;
486+
487+ const distinct = new Set < string > ( ) ;
488+ let numericCount = 0 ;
489+
490+ const isFiniteNumber = ( v : string ) : boolean => {
491+ const n = Number ( v ) ;
492+ return Number . isFinite ( n ) ;
493+ } ;
494+
495+ for ( const selection of selections ) {
496+ for ( let row = selection . fromRow ; row <= selection . toRow ; row ++ ) {
497+ for ( let col = selection . fromCell ; col <= selection . toCell ; col ++ ) {
498+ const cell = grid . getCellNode ( row , col ) ;
499+ if ( ! cell ) {
500+ continue ;
501+ }
502+
503+ summary . count ++ ;
504+ const cellText = cell . innerText ;
505+
506+ if ( cellText === "NULL" || cellText === null || cellText === undefined ) {
507+ summary . nullCount ++ ;
508+ continue ;
509+ }
510+
511+ distinct . add ( cellText ) ;
512+
513+ if ( isFiniteNumber ( cellText ) ) {
514+ const n = Number ( cellText ) ;
515+ numericCount ++ ;
516+ summary . sum += n ;
517+ if ( n < summary . min ) summary . min = n ;
518+ if ( n > summary . max ) summary . max = n ;
519+ }
520+ }
521+ }
522+ }
523+
524+ summary . distinctCount = distinct . size ;
525+
526+ // Only compute average when we actually saw numeric cells
527+ if ( numericCount > 0 ) {
528+ summary . average = ( summary . sum / numericCount ) . toFixed ( 3 ) ;
529+ } else {
530+ summary . average = "" ;
531+ }
532+
533+ // Normalize min/max if there were no numeric values
534+ if ( ! Number . isFinite ( summary . min ) ) summary . min = 0 ;
535+ if ( ! Number . isFinite ( summary . max ) ) summary . max = 0 ;
536+
537+ return summary ;
538+ }
539+
540+ /**
541+ * Calculate selection summary statistics from database row data
542+ * @param rowIdToRowMap Map of row IDs to database cell value arrays
543+ * @param rowIdToSelectionMap Map of row IDs to selection ranges
544+ * @returns SelectionSummaryStats Summary statistics
545+ */
546+ export function calculateSelectionSummaryFromData (
547+ rowIdToRowMap : Map < number , qr . DbCellValue [ ] > ,
548+ rowIdToSelectionMap : Map < number , qr . ISlickRange [ ] > ,
549+ ) : qr . SelectionSummaryStats {
550+ const summary : qr . SelectionSummaryStats = {
551+ count : 0 ,
552+ average : "" ,
553+ sum : 0 ,
554+ min : Infinity ,
555+ max : - Infinity ,
556+ removeSelectionStats : false ,
557+ distinctCount : 0 ,
558+ nullCount : 0 ,
559+ } ;
560+
561+ if ( rowIdToRowMap . size === 0 ) {
562+ // No data; normalize min/max to 0 to match prior behavior
563+ summary . min = 0 ;
564+ summary . max = 0 ;
565+ return summary ;
566+ }
567+
568+ const distinct = new Set < string > ( ) ;
569+ let numericCount = 0 ;
570+
571+ const isFiniteNumber = ( v : string ) : boolean => {
572+ const n = Number ( v ) ;
573+ return Number . isFinite ( n ) ;
574+ } ;
575+
576+ for ( const [ rowId , row ] of rowIdToRowMap ) {
577+ const rowSelections = rowIdToSelectionMap . get ( rowId ) ?? [ ] ;
578+ for ( const sel of rowSelections ) {
579+ const start = Math . max ( 0 , sel . fromCell ) ;
580+ const end = Math . min ( row . length - 1 , sel . toCell ) ;
581+ for ( let c = start ; c <= end ; c ++ ) {
582+ const cell = row [ c ] ;
583+ summary . count ++ ;
584+
585+ if ( cell ?. isNull ) {
586+ summary . nullCount ++ ;
587+ continue ;
588+ }
589+
590+ const display = cell ?. displayValue ?? "" ;
591+ distinct . add ( display ) ;
592+
593+ if ( isFiniteNumber ( display ) ) {
594+ const n = Number ( display ) ;
595+ numericCount ++ ;
596+ summary . sum += n ;
597+ if ( n < summary . min ) summary . min = n ;
598+ if ( n > summary . max ) summary . max = n ;
599+ } else {
600+ // There is at least one non-numeric (non-null) value
601+ summary . removeSelectionStats = false ;
602+ }
603+ }
604+ }
605+ }
606+
607+ summary . distinctCount = distinct . size ;
608+
609+ // Only compute average when we actually saw numeric cells and round to 2 decimal places
610+ summary . average = numericCount > 0 ? ( summary . sum / numericCount ) . toFixed ( 2 ) : "" ;
611+
612+ // Normalize min/max if there were no numeric values
613+ if ( ! Number . isFinite ( summary . min ) ) summary . min = 0 ;
614+ if ( ! Number . isFinite ( summary . max ) ) summary . max = 0 ;
615+
616+ return summary ;
617+ }
0 commit comments