From b4fee8d4a4a1489ef3ff8ad3711e0f460880c272 Mon Sep 17 00:00:00 2001 From: Anil Sahoo Date: Wed, 18 Feb 2026 11:27:16 +0530 Subject: [PATCH 1/6] Fixed an issue where Geometry Viewer was showing stale data and not auto-updating on query reruns or new query runs with new data or different geometry columns in Query tool. #9392 --- .../js/components/sections/GeometryViewer.jsx | 97 +++++++++++++++++-- .../js/components/sections/ResultSet.jsx | 72 ++++++++++---- 2 files changed, 144 insertions(+), 25 deletions(-) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx index 8842d6d287c..5aa12ee35e5 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx @@ -436,25 +436,110 @@ export function GeometryViewer({rows, columns, column}) { const mapRef = React.useRef(); const contentRef = React.useRef(); - const data = parseData(rows, columns, column); const queryToolCtx = React.useContext(QueryToolContext); + // Track previous state to detect changes + const prevStateRef = React.useRef({ + columnKey: null, + columnNames: null, + selectedRowPKs: [], + }); + + const [mapKey, setMapKey] = React.useState(0); + const currentColumnKey = column?.key; + const currentColumnNames = React.useMemo( + () => columns.map(c => c.key).sort().join(','), + [columns] + ); + + // Detect when to clear, filter, or re-render the map based on changes in geometry column, columns list, or rows + useEffect(() => { + const prevState = prevStateRef.current; + + if (!currentColumnKey) { + setMapKey(prev => prev + 1); + prevStateRef.current = { + columnKey: null, + columnNames: null, + selectedRowPKs: [], + }; + return; + } + + if (currentColumnKey !== prevState.columnKey || + currentColumnNames !== prevState.columnNames) { + setMapKey(prev => prev + 1); + prevStateRef.current = { + columnKey: currentColumnKey, + columnNames: currentColumnNames, + selectedRowPKs: [], + }; + return; + } + + if (currentColumnKey === prevState.columnKey && + currentColumnNames === prevState.columnNames && + rows.length > 0) { + + // If user previously selected specific rows, filter them from new data + if (prevState.selectedRowPKs.length > 0 && prevState.selectedRowPKs.length < rows.length) { + const newSelectedPKs = rows + .filter(row => prevState.selectedRowPKs.includes(row.__temp_PK)) + .map(row => row.__temp_PK); + + prevStateRef.current.selectedRowPKs = newSelectedPKs.length > 0 ? newSelectedPKs : rows.map(r => r.__temp_PK); + } else { + // All rows are displayed + const allPKs = rows.map(r => r.__temp_PK); + prevStateRef.current.selectedRowPKs = allPKs; + } + } + }, [currentColumnKey, currentColumnNames, rows]); + + const displayRows = React.useMemo(() => { + if (!currentColumnKey || rows.length === 0) return []; + + const selectedPKs = prevStateRef.current.selectedRowPKs; + return selectedPKs.length > 0 && selectedPKs.length < rows.length + ? rows.filter(row => selectedPKs.includes(row.__temp_PK)) + : rows; + }, [rows, currentColumnKey]); + + // Parse geometry data only when needed + const data = React.useMemo(() => { + if (!currentColumnKey) { + return { + 'geoJSONs': [], + 'selectedSRID': 0, + 'getPopupContent': undefined, + 'infoList': [gettext('Select a geometry/geography column to visualize.')], + }; + } + return parseData(displayRows, columns, column); + }, [displayRows, columns, column, currentColumnKey]); + useEffect(()=>{ let timeoutId; const contentResizeObserver = new ResizeObserver(()=>{ clearTimeout(timeoutId); - if(queryToolCtx.docker.isTabVisible(PANELS.GEOMETRY)) { + if(queryToolCtx?.docker?.isTabVisible(PANELS.GEOMETRY)) { timeoutId = setTimeout(function () { mapRef.current?.invalidateSize(); }, 100); } }); - contentResizeObserver.observe(contentRef.current); - }, []); + if(contentRef.current) { + contentResizeObserver.observe(contentRef.current); + } + return () => { + clearTimeout(timeoutId); + contentResizeObserver.disconnect(); + }; + }, [queryToolCtx]); - // Dyanmic CRS is not supported. Use srid as key and recreate the map on change + // Dyanmic CRS is not supported. Use srid and mapKey as key and recreate the map on change return ( - + { isDataChangedRef.current = Boolean(_.size(dataChangeStore.updated) || _.size(dataChangeStore.added) || _.size(dataChangeStore.deleted)); }, [dataChangeStore]); @@ -1460,30 +1463,61 @@ export function ResultSet() { return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_ADD_ROWS, triggerAddRows); }, [columns, selectedRows.size]); + const getFilteredRowsForGeometryViewer = React.useCallback(() => { + let selRowsData = rows; + if(selectedRows.size != 0) { + selRowsData = rows.filter((r)=>selectedRows.has(rowKeyGetter(r))); + } else if(selectedColumns.size > 0) { + let selectedCols = _.filter(columns, (_c, i)=>selectedColumns.has(i+1)); + selRowsData = _.map(rows, (r)=>_.pick(r, _.map(selectedCols, (c)=>c.key))); + } else if(selectedRange.current) { + let [,, startRowIdx, endRowIdx] = getRangeIndexes(); + selRowsData = rows.slice(startRowIdx, endRowIdx+1); + } else if(selectedCell.current?.[0]) { + selRowsData = [selectedCell.current[0]]; + } + return selRowsData; + }, [rows, columns, selectedRows, selectedColumns]); + + const openGeometryViewerTab = React.useCallback((column, rowsData) => { + layoutDocker.openTab({ + id: PANELS.GEOMETRY, + title: gettext('Geometry Viewer'), + content: , + closable: true, + }, PANELS.MESSAGES, 'after-tab', true); + }, [layoutDocker, columns]); + + // Handle manual Geometry Viewer opening useEffect(()=>{ const renderGeometries = (column)=>{ - let selRowsData = rows; - if(selectedRows.size != 0) { - selRowsData = rows.filter((r)=>selectedRows.has(rowKeyGetter(r))); - } else if(selectedColumns.size > 0) { - let selectedCols = _.filter(columns, (_c, i)=>selectedColumns.has(i+1)); - selRowsData = _.map(rows, (r)=>_.pick(r, _.map(selectedCols, (c)=>c.key))); - } else if(selectedRange.current) { - let [,, startRowIdx, endRowIdx] = getRangeIndexes(); - selRowsData = rows.slice(startRowIdx, endRowIdx+1); - } else if(selectedCell.current?.[0]) { - selRowsData = [selectedCell.current[0]]; - } - layoutDocker.openTab({ - id: PANELS.GEOMETRY, - title:gettext('Geometry Viewer'), - content: , - closable: true, - }, PANELS.MESSAGES, 'after-tab', true); + const selRowsData = getFilteredRowsForGeometryViewer(); + openGeometryViewerTab(column, selRowsData); }; eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries); return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries); - }, [rows, columns, selectedRows.size, selectedColumns.size]); + }, [getFilteredRowsForGeometryViewer, openGeometryViewerTab, eventBus]); + + // Auto-update Geometry Viewer when rows/columns change + useEffect(()=>{ + const rowsChanged = prevRowsRef.current !== rows; + const columnsChanged = prevColumnsRef.current !== columns; + const currentGeometryColumn = columns.find(col => col.cell === 'geometry' || col.cell === 'geography'); + + if((rowsChanged || columnsChanged) && layoutDocker.isTabOpen(PANELS.GEOMETRY)) { + + if(currentGeometryColumn) { + const selRowsData = getFilteredRowsForGeometryViewer(); + openGeometryViewerTab(currentGeometryColumn, selRowsData); + } else { + // No geometry column + openGeometryViewerTab(null, []); + } + } + + prevRowsRef.current = rows; + prevColumnsRef.current = columns; + }, [rows, columns, getFilteredRowsForGeometryViewer, openGeometryViewerTab, layoutDocker]); const triggerResetScroll = () => { // Reset the scroll position to previously saved location. From a6b77a8f7e33ef6988e2f08a55eaff2ef955804d Mon Sep 17 00:00:00 2001 From: Anil Sahoo Date: Wed, 18 Feb 2026 18:47:04 +0530 Subject: [PATCH 2/6] Fixed coderabbit comments and removed the __temp_PK to identifier based of first column or rowdata --- .../js/components/sections/GeometryViewer.jsx | 80 +++++++++++++------ 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx index 5aa12ee35e5..78301f49b73 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx @@ -6,7 +6,7 @@ // This software is released under the PostgreSQL Licence // ////////////////////////////////////////////////////////////// -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useMemo } from 'react'; import { styled } from '@mui/material/styles'; import ReactDOMServer from 'react-dom/server'; import _ from 'lodash'; @@ -49,6 +49,8 @@ const StyledBox = styled(Box)(({theme}) => ({ }, })); +const PK_COLUMN_NAMES = ['id', 'oid', 'ctid']; + function parseEwkbData(rows, column) { let key = column.key; const maxRenderByteLength = 20 * 1024 * 1024; //render geometry data up to 20MB @@ -191,6 +193,33 @@ function parseData(rows, columns, column) { }; } +// Find primary key column from columns array +function findPkColumn(columns) { + return columns.find(c => PK_COLUMN_NAMES.includes(c.name)); +} + +// Get unique row identifier using PK column or first column +function getRowIdentifier(row, pkColumn, columns) { + if (pkColumn?.key && row[pkColumn.key] !== undefined) { + return row[pkColumn.key]; + } + const firstKey = columns[0]?.key; + return firstKey && row[firstKey] !== undefined ? row[firstKey] : JSON.stringify(row); +} + +// Create Set of row identifiers +function createIdentifierSet(rowData, pkColumn, columns) { + return new Set(rowData.map(row => getRowIdentifier(row, pkColumn, columns))); +} + +// Match rows from previous selection to current rows +function matchRowSelection(prevRowData, currentRows, pkColumn, columns) { + if (prevRowData.length === 0) return []; + + const prevIdSet = createIdentifierSet(prevRowData, pkColumn, columns); + return currentRows.filter(row => prevIdSet.has(getRowIdentifier(row, pkColumn, columns))); +} + function PopupTable({data}) { return ( @@ -438,20 +467,22 @@ export function GeometryViewer({rows, columns, column}) { const contentRef = React.useRef(); const queryToolCtx = React.useContext(QueryToolContext); - // Track previous state to detect changes + // Track previous column state AND selected row data const prevStateRef = React.useRef({ columnKey: null, columnNames: null, - selectedRowPKs: [], + selectedRowData: [], }); const [mapKey, setMapKey] = React.useState(0); - const currentColumnKey = column?.key; + const currentColumnKey = useMemo(() => column?.key, [column]); const currentColumnNames = React.useMemo( () => columns.map(c => c.key).sort().join(','), [columns] ); + const pkColumn = useMemo(() => findPkColumn(columns), [columns]); + // Detect when to clear, filter, or re-render the map based on changes in geometry column, columns list, or rows useEffect(() => { const prevState = prevStateRef.current; @@ -461,7 +492,7 @@ export function GeometryViewer({rows, columns, column}) { prevStateRef.current = { columnKey: null, columnNames: null, - selectedRowPKs: [], + selectedRowData: [], }; return; } @@ -472,7 +503,7 @@ export function GeometryViewer({rows, columns, column}) { prevStateRef.current = { columnKey: currentColumnKey, columnNames: currentColumnNames, - selectedRowPKs: [], + selectedRowData: [], }; return; } @@ -480,30 +511,31 @@ export function GeometryViewer({rows, columns, column}) { if (currentColumnKey === prevState.columnKey && currentColumnNames === prevState.columnNames && rows.length > 0) { - - // If user previously selected specific rows, filter them from new data - if (prevState.selectedRowPKs.length > 0 && prevState.selectedRowPKs.length < rows.length) { - const newSelectedPKs = rows - .filter(row => prevState.selectedRowPKs.includes(row.__temp_PK)) - .map(row => row.__temp_PK); - - prevStateRef.current.selectedRowPKs = newSelectedPKs.length > 0 ? newSelectedPKs : rows.map(r => r.__temp_PK); + let newSelectedRowData; + if (prevState.selectedRowData.length === 0) { + // No previous selection, show all rows + newSelectedRowData = rows; + } else if (prevState.selectedRowData.length < rows.length) { + const matched = matchRowSelection(prevState.selectedRowData, rows, pkColumn, columns); + newSelectedRowData = matched.length > 0 ? matched : rows; } else { - // All rows are displayed - const allPKs = rows.map(r => r.__temp_PK); - prevStateRef.current.selectedRowPKs = allPKs; + newSelectedRowData = rows; } + prevStateRef.current.selectedRowData = newSelectedRowData; } - }, [currentColumnKey, currentColumnNames, rows]); + }, [currentColumnKey, currentColumnNames, rows, pkColumn, columns]); + // Get rows to display based on selection const displayRows = React.useMemo(() => { if (!currentColumnKey || rows.length === 0) return []; + const prevState = prevStateRef.current; + if (currentColumnKey !== prevState.columnKey || currentColumnNames !== prevState.columnNames) { + return rows; + } - const selectedPKs = prevStateRef.current.selectedRowPKs; - return selectedPKs.length > 0 && selectedPKs.length < rows.length - ? rows.filter(row => selectedPKs.includes(row.__temp_PK)) - : rows; - }, [rows, currentColumnKey]); + const selected = prevState.selectedRowData; + return selected.length > 0 && selected.length < rows.length ? selected : rows; + }, [rows, currentColumnKey, currentColumnNames]); // Parse geometry data only when needed const data = React.useMemo(() => { @@ -537,7 +569,7 @@ export function GeometryViewer({rows, columns, column}) { }; }, [queryToolCtx]); - // Dyanmic CRS is not supported. Use srid and mapKey as key and recreate the map on change + // Dynamic CRS is not supported. Use srid and mapKey as key and recreate the map on change return ( Date: Thu, 19 Feb 2026 10:10:16 +0530 Subject: [PATCH 3/6] Fixed some minor data assignment issue in react states --- .../static/js/components/sections/GeometryViewer.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx index 78301f49b73..85e8d42314a 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx @@ -49,7 +49,7 @@ const StyledBox = styled(Box)(({theme}) => ({ }, })); -const PK_COLUMN_NAMES = ['id', 'oid', 'ctid']; +const PK_COLUMN_NAMES = ['id', 'oid']; function parseEwkbData(rows, column) { let key = column.key; @@ -503,7 +503,7 @@ export function GeometryViewer({rows, columns, column}) { prevStateRef.current = { columnKey: currentColumnKey, columnNames: currentColumnNames, - selectedRowData: [], + selectedRowData: rows, }; return; } From aae18b2102e14cc12658b0bf2bc88d14b1079b80 Mon Sep 17 00:00:00 2001 From: Anil Sahoo Date: Thu, 19 Feb 2026 11:55:27 +0530 Subject: [PATCH 4/6] Fixed more coderabbit review comments for some scenario it suggested --- .../js/components/sections/GeometryViewer.jsx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx index 85e8d42314a..a27923872de 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx @@ -511,17 +511,7 @@ export function GeometryViewer({rows, columns, column}) { if (currentColumnKey === prevState.columnKey && currentColumnNames === prevState.columnNames && rows.length > 0) { - let newSelectedRowData; - if (prevState.selectedRowData.length === 0) { - // No previous selection, show all rows - newSelectedRowData = rows; - } else if (prevState.selectedRowData.length < rows.length) { - const matched = matchRowSelection(prevState.selectedRowData, rows, pkColumn, columns); - newSelectedRowData = matched.length > 0 ? matched : rows; - } else { - newSelectedRowData = rows; - } - prevStateRef.current.selectedRowData = newSelectedRowData; + prevStateRef.current.selectedRowData = displayRows; } }, [currentColumnKey, currentColumnNames, rows, pkColumn, columns]); @@ -532,10 +522,15 @@ export function GeometryViewer({rows, columns, column}) { if (currentColumnKey !== prevState.columnKey || currentColumnNames !== prevState.columnNames) { return rows; } - - const selected = prevState.selectedRowData; - return selected.length > 0 && selected.length < rows.length ? selected : rows; - }, [rows, currentColumnKey, currentColumnNames]); + + const prevSelected = prevState.selectedRowData; + if (prevSelected.length === 0) return rows; + if (prevSelected.length < rows.length) { + const matched = matchRowSelection(prevSelected, rows, pkColumn, columns); + return matched.length > 0 ? matched : rows; + } + return rows; + }, [rows, currentColumnKey, currentColumnNames, pkColumn, columns]); // Parse geometry data only when needed const data = React.useMemo(() => { From bc672e2b9900cfa636d007b9fe8434a6039218dd Mon Sep 17 00:00:00 2001 From: Anil Sahoo Date: Fri, 20 Feb 2026 14:03:30 +0530 Subject: [PATCH 5/6] Fixed a scenario where different query with different column execution will return blank geometry viewer --- .../js/components/sections/GeometryViewer.jsx | 26 +++++++++++- .../js/components/sections/ResultSet.jsx | 40 ++++++++++++++++--- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx index a27923872de..9f67d72455b 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx @@ -323,6 +323,27 @@ function TheMap({data}) { infoControl.current.onAdd = function () { let ele = Leaflet.DomUtil.create('div', 'geometry-viewer-info-control'); ele.innerHTML = data.infoList.join('
'); + // Style the parent control container after it's added to the map + setTimeout(() => { + let controlContainer = ele.closest('.leaflet-control'); + if(controlContainer) { + controlContainer.style.cssText = ` + position: fixed; + top: 70%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0; + max-width: 80%; + text-align: center; + white-space: normal; + word-wrap: break-word; + background: none; + box-shadow: none; + border: none; + font-size: 16px; + `; + } + }, 0); return ele; }; if(data.infoList.length > 0) { @@ -535,11 +556,14 @@ export function GeometryViewer({rows, columns, column}) { // Parse geometry data only when needed const data = React.useMemo(() => { if (!currentColumnKey) { + const hasGeometryColumn = columns.some(c => c.cell === 'geometry' || c.cell === 'geography'); return { 'geoJSONs': [], 'selectedSRID': 0, 'getPopupContent': undefined, - 'infoList': [gettext('Select a geometry/geography column to visualize.')], + 'infoList': hasGeometryColumn + ? [gettext('Query complete. Use the Geometry Viewer button in the Data Output tab to visualize results.')] + : [gettext('No spatial data found. At least one geometry or geography column is required for visualization.')], }; } return parseData(displayRows, columns, column); diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx index 752a5425017..dba048f2846 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx @@ -878,6 +878,11 @@ export function ResultSet() { const isDataChangedRef = useRef(false); const prevRowsRef = React.useRef(null); const prevColumnsRef = React.useRef(null); + const gvClearedForColumnsRef = useRef(null); + const lastGvSelectionRef = useRef({ + type: 'all', // 'all' | 'rows' | 'columns' + selectedColumns: new Set(), + }); useEffect(()=>{ isDataChangedRef.current = Boolean(_.size(dataChangeStore.updated) || _.size(dataChangeStore.added) || _.size(dataChangeStore.deleted)); @@ -1463,13 +1468,19 @@ export function ResultSet() { return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_ADD_ROWS, triggerAddRows); }, [columns, selectedRows.size]); - const getFilteredRowsForGeometryViewer = React.useCallback(() => { + const getFilteredRowsForGeometryViewer = React.useCallback((useLastGvSelection = false) => { let selRowsData = rows; if(selectedRows.size != 0) { selRowsData = rows.filter((r)=>selectedRows.has(rowKeyGetter(r))); } else if(selectedColumns.size > 0) { let selectedCols = _.filter(columns, (_c, i)=>selectedColumns.has(i+1)); selRowsData = _.map(rows, (r)=>_.pick(r, _.map(selectedCols, (c)=>c.key))); + } else if(useLastGvSelection && lastGvSelectionRef.current.type === 'columns' + && lastGvSelectionRef.current.selectedColumns.size > 0) { + let selectedCols = _.filter(columns, (_c, i)=>lastGvSelectionRef.current.selectedColumns.has(i+1)); + if(selectedCols.length > 0) { + selRowsData = _.map(rows, (r)=>_.pick(r, _.map(selectedCols, (c)=>c.key))); + } } else if(selectedRange.current) { let [,, startRowIdx, endRowIdx] = getRangeIndexes(); selRowsData = rows.slice(startRowIdx, endRowIdx+1); @@ -1491,12 +1502,20 @@ export function ResultSet() { // Handle manual Geometry Viewer opening useEffect(()=>{ const renderGeometries = (column)=>{ + gvClearedForColumnsRef.current = null; + if(selectedRows.size > 0) { + lastGvSelectionRef.current = { type: 'rows', selectedColumns: new Set() }; + } else if(selectedColumns.size > 0) { + lastGvSelectionRef.current = { type: 'columns', selectedColumns: new Set(selectedColumns) }; + } else { + lastGvSelectionRef.current = { type: 'all', selectedColumns: new Set() }; + } const selRowsData = getFilteredRowsForGeometryViewer(); openGeometryViewerTab(column, selRowsData); }; eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries); return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries); - }, [getFilteredRowsForGeometryViewer, openGeometryViewerTab, eventBus]); + }, [getFilteredRowsForGeometryViewer, openGeometryViewerTab, eventBus, selectedRows, selectedColumns]); // Auto-update Geometry Viewer when rows/columns change useEffect(()=>{ @@ -1506,8 +1525,19 @@ export function ResultSet() { if((rowsChanged || columnsChanged) && layoutDocker.isTabOpen(PANELS.GEOMETRY)) { - if(currentGeometryColumn) { - const selRowsData = getFilteredRowsForGeometryViewer(); + const prevColumnNames = prevColumnsRef.current?.map(c => c.key).sort().join(',') ?? ''; + const currColumnNames = columns.map(c => c.key).sort().join(','); + const columnsChanged = prevColumnNames !== currColumnNames; + + if(columnsChanged && currentGeometryColumn) { + gvClearedForColumnsRef.current = currColumnNames; + lastGvSelectionRef.current = { type: 'all', selectedColumns: new Set() }; + openGeometryViewerTab(null, []); + } else if(gvClearedForColumnsRef.current === currColumnNames) { + openGeometryViewerTab(null, []); + } else if(currentGeometryColumn && rowsChanged) { + const useColSelection = lastGvSelectionRef.current.type === 'columns'; + const selRowsData = getFilteredRowsForGeometryViewer(useColSelection); openGeometryViewerTab(currentGeometryColumn, selRowsData); } else { // No geometry column @@ -1517,7 +1547,7 @@ export function ResultSet() { prevRowsRef.current = rows; prevColumnsRef.current = columns; - }, [rows, columns, getFilteredRowsForGeometryViewer, openGeometryViewerTab, layoutDocker]); + }, [rows, columns, getFilteredRowsForGeometryViewer, layoutDocker]); const triggerResetScroll = () => { // Reset the scroll position to previously saved location. From 217771f40300721d38cd0b7c189bb9777eb17ffe Mon Sep 17 00:00:00 2001 From: Anil Sahoo Date: Tue, 24 Feb 2026 11:17:48 +0530 Subject: [PATCH 6/6] Fixed review comments 1. Used hash function in place of JSON.stringify(row) 2. Storing row identifiers in place of complete row object. 3. Removed Leaflet's infoControl and added a react div --- .../js/components/sections/GeometryViewer.jsx | 113 ++++++++++-------- 1 file changed, 62 insertions(+), 51 deletions(-) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx index 9f67d72455b..506a95c670b 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx @@ -22,6 +22,7 @@ import { PANELS } from '../QueryToolConstants'; import { QueryToolContext } from '../QueryToolComponent'; const StyledBox = styled(Box)(({theme}) => ({ + position: 'relative', '& .GeometryViewer-mapContainer': { backgroundColor: theme.palette.background.default, height: '100%', @@ -193,31 +194,40 @@ function parseData(rows, columns, column) { }; } -// Find primary key column from columns array +// Find primary key column i.e a column with unique values from columns array in Data Output tab function findPkColumn(columns) { return columns.find(c => PK_COLUMN_NAMES.includes(c.name)); } +// Hash function for row objects +function hashRow(row) { + const str = Object.keys(row).sort().map(k => `${k}:${row[k]}`).join('|'); + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + return `hash_${hash}`; +} + // Get unique row identifier using PK column or first column function getRowIdentifier(row, pkColumn, columns) { if (pkColumn?.key && row[pkColumn.key] !== undefined) { return row[pkColumn.key]; } const firstKey = columns[0]?.key; - return firstKey && row[firstKey] !== undefined ? row[firstKey] : JSON.stringify(row); -} - -// Create Set of row identifiers -function createIdentifierSet(rowData, pkColumn, columns) { - return new Set(rowData.map(row => getRowIdentifier(row, pkColumn, columns))); + if (firstKey && row[firstKey] !== undefined) { + return row[firstKey]; + } + return hashRow(row); } // Match rows from previous selection to current rows -function matchRowSelection(prevRowData, currentRows, pkColumn, columns) { - if (prevRowData.length === 0) return []; +function matchRowSelection(prevIdentifiers, currentRows, pkColumn, columns) { + if (prevIdentifiers.size === 0) return []; - const prevIdSet = createIdentifierSet(prevRowData, pkColumn, columns); - return currentRows.filter(row => prevIdSet.has(getRowIdentifier(row, pkColumn, columns))); + return currentRows.filter(row => prevIdentifiers.has(getRowIdentifier(row, pkColumn, columns))); } function PopupTable({data}) { @@ -314,41 +324,10 @@ GeoJsonLayer.propTypes = { function TheMap({data}) { const mapObj = useMap(); - const infoControl = useRef(null); const resetLayersKey = useRef(0); const zoomControlWithHome = useRef(null); const homeCoordinates = useRef(null); useEffect(()=>{ - infoControl.current = Leaflet.control({position: 'topright'}); - infoControl.current.onAdd = function () { - let ele = Leaflet.DomUtil.create('div', 'geometry-viewer-info-control'); - ele.innerHTML = data.infoList.join('
'); - // Style the parent control container after it's added to the map - setTimeout(() => { - let controlContainer = ele.closest('.leaflet-control'); - if(controlContainer) { - controlContainer.style.cssText = ` - position: fixed; - top: 70%; - left: 50%; - transform: translate(-50%, -50%); - margin: 0; - max-width: 80%; - text-align: center; - white-space: normal; - word-wrap: break-word; - background: none; - box-shadow: none; - border: none; - font-size: 16px; - `; - } - }, 0); - return ele; - }; - if(data.infoList.length > 0) { - infoControl.current.addTo(mapObj); - } resetLayersKey.current++; zoomControlWithHome.current = Leaflet.control.zoom({ @@ -398,7 +377,6 @@ function TheMap({data}) { zoomControlWithHome.current.addTo(mapObj); return ()=>{ - infoControl.current?.remove(); zoomControlWithHome.current?.remove(); }; }, [data]); @@ -409,6 +387,25 @@ function TheMap({data}) { return ( <> + {data.infoList.length > 0 && ( +
+ {data.infoList.map((info, idx) => ( +
{info}
+ ))} +
+ )} {data.selectedSRID === 4326 && @@ -492,7 +489,7 @@ export function GeometryViewer({rows, columns, column}) { const prevStateRef = React.useRef({ columnKey: null, columnNames: null, - selectedRowData: [], + selectedRowIdentifiers: new Set(), }); const [mapKey, setMapKey] = React.useState(0); @@ -513,7 +510,7 @@ export function GeometryViewer({rows, columns, column}) { prevStateRef.current = { columnKey: null, columnNames: null, - selectedRowData: [], + selectedRowIdentifiers: new Set(), }; return; } @@ -524,7 +521,7 @@ export function GeometryViewer({rows, columns, column}) { prevStateRef.current = { columnKey: currentColumnKey, columnNames: currentColumnNames, - selectedRowData: rows, + selectedRowIdentifiers: new Set(rows.map(r => getRowIdentifier(r, pkColumn, columns))), }; return; } @@ -532,24 +529,38 @@ export function GeometryViewer({rows, columns, column}) { if (currentColumnKey === prevState.columnKey && currentColumnNames === prevState.columnNames && rows.length > 0) { - prevStateRef.current.selectedRowData = displayRows; + prevStateRef.current.selectedRowIdentifiers = new Set( + displayRows.map(r => getRowIdentifier(r, pkColumn, columns)) + ); } }, [currentColumnKey, currentColumnNames, rows, pkColumn, columns]); // Get rows to display based on selection const displayRows = React.useMemo(() => { + // No geometry column selected or no rows available - nothing to display if (!currentColumnKey || rows.length === 0) return []; const prevState = prevStateRef.current; + + // Column context changed (different geometry column or different query schema) + // Show all new rows since previous selection is no longer valid if (currentColumnKey !== prevState.columnKey || currentColumnNames !== prevState.columnNames) { return rows; } - const prevSelected = prevState.selectedRowData; - if (prevSelected.length === 0) return rows; - if (prevSelected.length < rows.length) { - const matched = matchRowSelection(prevSelected, rows, pkColumn, columns); + const prevIdentifiers = prevState.selectedRowIdentifiers; + // No previous selection recorded - show all rows + if (prevIdentifiers.size === 0) return rows; + + // Previous selection was a subset of total rows, meaning user had specific rows selected. + // Try to match those previously selected rows in the new result set using stable + // row identifiers (PK value, first column value, or hash fallback). + // This handles the case where same query reruns with more/fewer rows + if (prevIdentifiers.size < rows.length) { + const matched = matchRowSelection(prevIdentifiers, rows, pkColumn, columns); + // If matched rows found, show only those; otherwise fall back to all rows return matched.length > 0 ? matched : rows; } + // Previous selection covered all rows (or same count) - show all current rows return rows; }, [rows, currentColumnKey, currentColumnNames, pkColumn, columns]);