diff --git a/docs/en_US/images/preferences_sql_results_grid.png b/docs/en_US/images/preferences_sql_results_grid.png index 6c4a64a7c..896af2962 100644 Binary files a/docs/en_US/images/preferences_sql_results_grid.png and b/docs/en_US/images/preferences_sql_results_grid.png differ diff --git a/docs/en_US/images/query_data_pagination.png b/docs/en_US/images/query_data_pagination.png new file mode 100644 index 000000000..5fa94e223 Binary files /dev/null and b/docs/en_US/images/query_data_pagination.png differ diff --git a/docs/en_US/images/query_data_pagination_edit.png b/docs/en_US/images/query_data_pagination_edit.png new file mode 100644 index 000000000..fc17656d3 Binary files /dev/null and b/docs/en_US/images/query_data_pagination_edit.png differ diff --git a/docs/en_US/images/query_output_data.png b/docs/en_US/images/query_output_data.png index 36575f1c9..f21363421 100644 Binary files a/docs/en_US/images/query_output_data.png and b/docs/en_US/images/query_output_data.png differ diff --git a/docs/en_US/preferences.rst b/docs/en_US/preferences.rst index 6ffa959e8..a608d1a95 100644 --- a/docs/en_US/preferences.rst +++ b/docs/en_US/preferences.rst @@ -230,7 +230,7 @@ Use the fields on the *Options* panel to manage ERD preferences. * Use *Cardinality Notation* to change the cardinality notation format - used to present relationship links. + used to present relationship links. * When the *SQL With DROP Table* switch is set to *True*, the SQL generated by the ERD Tool will add DROP table DDL before each CREATE @@ -398,7 +398,7 @@ Use the fields on the *Editor* panel to change settings of the query editor. changed to text/plain. Keyword highlighting and code folding will be disabled. This will improve editor performance with large files. -* When the *Highlight selection matches?* switch is set to *True*, the editor will +* When the *Highlight selection matches?* switch is set to *True*, the editor will highlight matched selected text. .. image:: images/preferences_sql_explain.png @@ -476,11 +476,11 @@ Use the fields on the *Options* panel to manage editor preferences. * When the *Show View/Edit Data Promotion Warning?* switch is set to *True* View/Edit Data tool will show promote to Query tool confirm dialog on query edit. -* When the *Underline query at cursor?* switch is set to *True*, query tool will +* When the *Underline query at cursor?* switch is set to *True*, query tool will parse and underline the query at the cursor position. -* When the *Underlined query execute warning?* switch is set to *True*, query tool - will warn upon clicking the *Execute Query* button in the query tool. The warning +* When the *Underlined query execute warning?* switch is set to *True*, query tool + will warn upon clicking the *Execute Query* button in the query tool. The warning will appear only if *Underline query at cursor?* is set to *False*. .. image:: images/preferences_sql_results_grid.png @@ -497,9 +497,8 @@ preferences for copied data. * Specify the maximum width of the column in pixels when 'Columns sized by' is set to *Column data*. If 'Columns sized by' is set to *Column name* then this setting won't have any effect. -* Specify the number of records to fetch in one batch in query tool when - query result set is large. Changing this value will override - ON_DEMAND_ROW_COUNT setting from config file. +* Specify the number of records to fetch in one batch. Changing this value will + override DATA_RESULT_ROWS_PER_PAGE setting from config file. * Use the *Result copy field separator* drop-down listbox to select the field separator for copied data. * Use the *Result copy quote character* drop-down listbox to select the quote @@ -523,7 +522,7 @@ reformatting of SQL. * Use the *Data type case* option to specify whether to change data types into upper, lower, or preserve case. -* Use the *Expression width* option to specify maximum number of characters +* Use the *Expression width* option to specify maximum number of characters in parenthesized expressions to be kept on single line. * Use the *Function case* option to specify whether to change function names into upper, lower, or preserve case. @@ -531,7 +530,7 @@ reformatting of SQL. (object names) into upper, lower, or capitalized case. * Use the *Keyword case* option to specify whether to change keywords into upper, lower, or preserve case. -* Use *Lines between queries* to specify how many empty lines to leave +* Use *Lines between queries* to specify how many empty lines to leave between SQL statements. If set to zero it puts no new line. * Use *Logical operator new line* to specify newline placement before or after logical operators (AND, OR, XOR). diff --git a/docs/en_US/query_tool.rst b/docs/en_US/query_tool.rst index be0e74fe5..88a0de9fb 100644 --- a/docs/en_US/query_tool.rst +++ b/docs/en_US/query_tool.rst @@ -74,8 +74,8 @@ key combination to select from a popup menu of autocomplete options. After entering a query, select the *Execute script* icon from the toolbar. The complete contents of the SQL editor panel will be sent to the database server -for execution. To execute only a section of the code that is displayed in the -SQL editor, highlight the text that you want the server to execute, and click the +for execution. To execute only a section of the code that is displayed in the +SQL editor, highlight the text that you want the server to execute, and click the *Execute script* icon. .. image:: images/query_execute_script.png @@ -177,6 +177,7 @@ You can: * Use the *Save results to file* icon to save the content of the *Data Output* tab as a comma-delimited file. * Edit the data in the result set of a SELECT query if it is updatable. +* Move between pages of data result. .. _updatable-result-set: diff --git a/docs/en_US/query_tool_toolbar.rst b/docs/en_US/query_tool_toolbar.rst index ccafd6334..9c693f0ad 100644 --- a/docs/en_US/query_tool_toolbar.rst +++ b/docs/en_US/query_tool_toolbar.rst @@ -214,6 +214,48 @@ Data Editing Options | SQL | Use the SQL button to check the current query that gave the data. | | +----------------------+---------------------------------------------------------------------------------------------------+----------------+ + +Pagination Options +******************** + +.. image:: images/query_data_pagination.png + :alt: Query tool data pagination options + :align: center + +.. table:: + :class: longtable + :widths: 1 4 1 + + +----------------------+---------------------------------------------------------------------------------------------------+----------------+ + | Icon | Behavior | Shortcut | + +======================+===================================================================================================+================+ + | *Rows Range* | Show the current row numbers visible in the data grid. | | + +----------------------+---------------------------------------------------------------------------------------------------+----------------+ + | *Edit Range* | Click to open the from and to rows range inputs to allow setting them. | | + +----------------------+---------------------------------------------------------------------------------------------------+----------------+ + | *Page No* | Enter the page no you want to jump to out of total shown next to this input | | + +----------------------+---------------------------------------------------------------------------------------------------+----------------+ + | *First Page* | Click to go to the first page. | | + +----------------------+---------------------------------------------------------------------------------------------------+----------------+ + | *Previous Page* | Click to go to the previous page. | | + +----------------------+---------------------------------------------------------------------------------------------------+----------------+ + | *Next Page* | Click to go to the next page. | | + +----------------------+---------------------------------------------------------------------------------------------------+----------------+ + | *Last Page* | Click to go to the last page. | | + +----------------------+---------------------------------------------------------------------------------------------------+----------------+ + + +.. image:: images/query_data_pagination_edit.png + :alt: Query tool data pagination options + :align: center + +One can click the edit range button to open rows range editor: + +* From and to range should be between 1 and total rows. +* The range can be applied by clicking the *Apply* button or by pressing enter in the range inputs. +* Once the range is applied, pgAdmin will recalculate the rows per page. The pagination will then behave based on the new rows per page. +* It may be possible that on pressing next page button, the new rows range is not next to manually enterred range. + Status Bar ********** diff --git a/web/config.py b/web/config.py index a49ba1f09..09ef74bc0 100644 --- a/web/config.py +++ b/web/config.py @@ -513,10 +513,10 @@ THREADED_MODE = True SQLALCHEMY_TRACK_MODIFICATIONS = False ########################################################################## -# Number of records to fetch in one batch in query tool when query result -# set is large. +# Number of records to fetch in one page in query tool when query result +# set is large and is divided in multiple pages ########################################################################## -ON_DEMAND_RECORD_COUNT = 1000 +DATA_RESULT_ROWS_PER_PAGE = 1000 ########################################################################## # Allow users to display Gravatar image for their username in Server mode diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index b571a34d1..a4a1df6e3 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -15,6 +15,7 @@ import secrets from urllib.parse import unquote from threading import Lock import threading +import math import json from config import PG_DEFAULT_DRIVER, ALLOW_SAVE_PASSWORD, SHARED_STORAGE @@ -106,8 +107,7 @@ class SqlEditorModule(PgAdminModule): 'sqleditor.view_data_start', 'sqleditor.query_tool_start', 'sqleditor.poll', - 'sqleditor.fetch', - 'sqleditor.fetch_all', + 'sqleditor.fetch_window', 'sqleditor.fetch_all_from_start', 'sqleditor.save', 'sqleditor.inclusive_filter', @@ -470,7 +470,8 @@ def _init_sqleditor(trans_id, connect, sgid, sid, did, dbname=None, **kwargs): "prompt_password": True, "allow_save_password": True if ALLOW_SAVE_PASSWORD and - session['allow_save_password'] else False, + session.get('allow_save_password', None) + else False, } ), '', '' else: @@ -925,7 +926,6 @@ def poll(trans_id): rows_affected = 0 rows_fetched_from = 0 rows_fetched_to = 0 - has_more_rows = False columns = dict() columns_info = None primary_keys = None @@ -936,8 +936,8 @@ def poll(trans_id): additional_messages = None notifies = None data_obj = {} - on_demand_record_count = Preferences.module(MODULE_NAME).\ - preference('on_demand_record_count').get() + data_result_rows_per_page = Preferences.module(MODULE_NAME).\ + preference('data_result_rows_per_page').get() # Check the transaction and connection status status, error_msg, conn, trans_obj, session_obj = \ check_transaction_status(trans_id) @@ -1004,7 +1004,8 @@ def poll(trans_id): status = 'Success' rows_affected = conn.rows_affected() - st, result = conn.async_fetchmany_2darray(on_demand_record_count) + st, result = \ + conn.async_fetchmany_2darray(data_result_rows_per_page) # There may be additional messages even if result is present # eg: Function can provide result as well as RAISE messages @@ -1081,8 +1082,6 @@ def poll(trans_id): # means nothing to fetch if result and rows_affected > -1: res_len = len(result) - if res_len == on_demand_record_count: - has_more_rows = True if res_len > 0: rows_fetched_from = trans_obj.get_fetched_row_cnt() @@ -1126,6 +1125,15 @@ def poll(trans_id): data_obj['db_id'] = trans_obj.did \ if trans_obj is not None and hasattr(trans_obj, 'did') else 0 + page_size = rows_fetched_to - rows_fetched_from + 1 + pagination = { + 'page_size': page_size, + 'page_count': math.ceil(conn.total_rows / page_size), + 'page_no': math.floor(rows_fetched_from / page_size) + 1, + 'rows_from': rows_fetched_from, + 'rows_to': rows_fetched_to + } + return make_json_response( data={ 'status': status, 'result': result, @@ -1134,7 +1142,6 @@ def poll(trans_id): 'rows_fetched_to': rows_fetched_to, 'additional_messages': additional_messages, 'notifies': notifies, - 'has_more_rows': has_more_rows, 'colinfo': columns_info, 'primary_keys': primary_keys, 'types': types, @@ -1143,26 +1150,20 @@ def poll(trans_id): 'oids': oids, 'transaction_status': transaction_status, 'data_obj': data_obj, + 'pagination': pagination, } ) @blueprint.route( - '/fetch/', methods=["GET"], endpoint='fetch' -) -@blueprint.route( - '/fetch//', methods=["GET"], - endpoint='fetch_all' + '/fetch_window///', + methods=["GET"], endpoint='fetch_window' ) @pga_login_required -def fetch(trans_id, fetch_all=None): +def fetch_window(trans_id, from_rownum=0, to_rownum=0): result = None - has_more_rows = False rows_fetched_from = 0 rows_fetched_to = 0 - on_demand_record_count = Preferences.module(MODULE_NAME).preference( - 'on_demand_record_count').get() - fetch_row_cnt = -1 if fetch_all == 1 else on_demand_record_count # Check the transaction and connection status status, error_msg, conn, trans_obj, session_obj = \ @@ -1174,33 +1175,39 @@ def fetch(trans_id, fetch_all=None): status=404) if status and conn is not None and session_obj is not None: - status, result = conn.async_fetchmany_2darray(fetch_row_cnt) + # rownums start from 0 but UI will ask from 1 + status, result = conn.async_fetchmany_2darray( + records=None, from_rownum=from_rownum - 1, to_rownum=to_rownum - 1) if not status: status = 'Error' else: status = 'Success' res_len = len(result) if result else 0 - if fetch_row_cnt != -1 and res_len == on_demand_record_count: - has_more_rows = True if res_len: - rows_fetched_from = trans_obj.get_fetched_row_cnt() - trans_obj.update_fetched_row_cnt(rows_fetched_from + res_len) - rows_fetched_from += 1 - rows_fetched_to = trans_obj.get_fetched_row_cnt() + rows_fetched_from = from_rownum + rows_fetched_to = rows_fetched_from + res_len - 1 session_obj['command_obj'] = pickle.dumps(trans_obj, -1) update_session_grid_transaction(trans_id, session_obj) else: status = 'NotConnected' result = error_msg + page_size = to_rownum - from_rownum + 1 + pagination = { + 'page_size': page_size, + 'page_count': math.ceil(conn.total_rows / page_size), + 'page_no': math.floor(rows_fetched_from / page_size) + 1, + 'rows_from': rows_fetched_from, + 'rows_to': rows_fetched_to + } + return make_json_response( data={ 'status': status, 'result': result, - 'has_more_rows': has_more_rows, - 'rows_fetched_from': rows_fetched_from, - 'rows_fetched_to': rows_fetched_to + 'pagination': pagination, + 'row_count': conn.row_count, } ) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js index 691332e48..e33514e07 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js @@ -37,7 +37,7 @@ export const QUERY_TOOL_EVENTS = { STOP_QUERY: 'STOP_QUERY', CURSOR_ACTIVITY: 'CURSOR_ACTIVITY', SET_MESSAGE: 'SET_MESSAGE', - ROWS_FETCHED: 'ROWS_FETCHED', + TOTAL_ROWS_COUNT: 'TOTAL_ROWS_COUNT', SELECTED_ROWS_COLS_CELL_CHANGED: 'SELECTED_ROWS_COLS_CELL_CHANGED', DATAGRID_CHANGED: 'DATAGRID_CHANGED', HIGHLIGHT_ERROR: 'HIGHLIGHT_ERROR', @@ -56,8 +56,12 @@ export const QUERY_TOOL_EVENTS = { PUSH_HISTORY: 'PUSH_HISTORY', HANDLE_API_ERROR: 'HANDLE_API_ERROR', SET_FILTER_INFO: 'SET_FILTER_INFO', - FETCH_MORE_ROWS: 'FETCH_MORE_ROWS', REINIT_QT_CONNECTION:'REINIT_QT_CONNECTION', + FETCH_WINDOW: 'FETCH_WINDOW', + ALL_PAGE_ROWS_SELECTED: 'ALL_PAGE_ROWS_SELECTED', + ALL_ROWS_SELECTED: 'ALL_ROWS_SELECTED', + CLEAR_ROWS_SELECTED: 'CLEAR_ROWS_SELECTED', + ALL_ROWS_SELECTED_STATUS: 'ALL_ROWS_SELECTED_STATUS', EDITOR_LAST_FOCUS: 'EDITOR_LAST_FOCUS', EDITOR_FIND_REPLACE: 'EDITOR_FIND_REPLACE', @@ -109,4 +113,4 @@ export const PANELS = { export const MAX_QUERY_LENGTH = 1000000; -export const OS_EOL = navigator.platform === 'win32' ? 'crlf' : 'lf'; \ No newline at end of file +export const OS_EOL = navigator.platform === 'win32' ? 'crlf' : 'lf'; diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/index.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/index.jsx index f027a3c33..da8931a27 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/index.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/index.jsx @@ -139,9 +139,8 @@ function SelectAllHeaderRenderer({isCellSelected}) { const eventBus = useContext(QueryToolEventsContext); const dataGridExtras = useContext(DataGridExtrasContext); const onClick = ()=>{ - eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, true, ()=>{ - onRowSelectionChange({ type: 'HEADER', checked: !isRowSelected }); - }); + onRowSelectionChange({ type: 'HEADER', checked: !isRowSelected }); + eventBus.fireEvent(QUERY_TOOL_EVENTS.ALL_PAGE_ROWS_SELECTED, !isRowSelected); }; useLayoutEffect(() => { @@ -149,6 +148,15 @@ function SelectAllHeaderRenderer({isCellSelected}) { cellRef.current?.focus({ preventScroll: true }); }, [isCellSelected]); + useEffect(()=>{ + const unregClear = eventBus.registerListener(QUERY_TOOL_EVENTS.CLEAR_ROWS_SELECTED, ()=>{ + onRowSelectionChange({ type: 'HEADER', checked: false }); + }); + return ()=>{ + unregClear(); + }; + }, []); + return
; } @@ -167,15 +175,13 @@ function SelectableHeaderRenderer({column, selectedColumns, onSelectedColumnsCha } const onClick = ()=>{ - eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, true, ()=>{ - const newSelectedCols = new Set(selectedColumns); - if (newSelectedCols.has(column.idx)) { - newSelectedCols.delete(column.idx); - } else { - newSelectedCols.add(column.idx); - } - onSelectedColumnsChange(newSelectedCols); - }); + const newSelectedCols = new Set(selectedColumns); + if (newSelectedCols.has(column.idx)) { + newSelectedCols.delete(column.idx); + } else { + newSelectedCols.add(column.idx); + } + onSelectedColumnsChange(newSelectedCols); }; const isSelected = selectedColumns.has(column.idx); @@ -296,9 +302,10 @@ function initialiseColumns(columns, rows, totalRowCount, columnWidthBy) { } function RowNumColFormatter({row, rowKeyGetter, rowIdx, dataChangeStore, onSelectedColumnsChange}) { const [isRowSelected, onRowSelectionChange] = useRowSelection(); + const {startRowNum} = useContext(DataGridExtrasContext); let rowKey = rowKeyGetter(row); - let rownum = rowIdx+1; + let rownum = rowIdx+(startRowNum??1); if(rowKey in (dataChangeStore?.added || {})) { rownum = rownum+'+'; } else if(rowKey in (dataChangeStore?.deleted || {})) { @@ -375,7 +382,7 @@ function getTextWidth(column, rows, canvas, columnWidthBy) { } export default function QueryToolDataGrid({columns, rows, totalRowCount, dataChangeStore, - onSelectedCellChange, selectedColumns, onSelectedColumnsChange, columnWidthBy, ...props}) { + onSelectedCellChange, selectedColumns, onSelectedColumnsChange, columnWidthBy, startRowNum, ...props}) { const [readyColumns, setReadyColumns] = useState([]); const eventBus = useContext(QueryToolEventsContext); const onSelectedColumnsChangeWrapped = (arg)=>{ @@ -392,7 +399,7 @@ export default function QueryToolDataGrid({columns, rows, totalRowCount, dataCha }, []); const dataGridExtras = useMemo(()=>({ - onSelectedCellChange, handleCopy + onSelectedCellChange, handleCopy, startRowNum }), [onSelectedCellChange]); useEffect(()=>{ diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx index 47f101cd9..df26869b6 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx @@ -276,10 +276,10 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT }, [queryToolConnCtx.connectionStatus]); const onCommitClick=()=>{ - eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'COMMIT;', null, '', true); + eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'COMMIT;', {external: true}); }; const onRollbackClick=()=>{ - eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'ROLLBACK;', null, '', true); + eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'ROLLBACK;', {external: true}); }; const executeMacro = (m)=>{ eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, null, m.sql); diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx index 737fe0d04..31125316e 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx @@ -151,10 +151,10 @@ export default function Query({onTextSelect, handleEndOfLineChange}) { query = query || editor.current?.getValue() || ''; } if(query) { - eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, macroSQL, external, null, executeCursor); + eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, {explainObject, macroSQL, external, executeCursor}); } } else { - eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, null, null, ''); + eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, null, {}); } }; 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 52a99a3df..cfc9683c7 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx @@ -232,15 +232,20 @@ export class ResultSetUtils { }; async startExecution(query, explainObject, macroSQL, onIncorrectSQL, flags={ - isQueryTool: true, external: false, reconnect: false, executeCursor: false + isQueryTool: true, external: false, reconnect: false, executeCursor: false, refreshData: false, }) { let startTime = new Date(); this.eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, ''); - this.eventBus.fireEvent(QUERY_TOOL_EVENTS.TASK_START, gettext('Waiting for the query to complete...'), startTime); + this.eventBus.fireEvent(QUERY_TOOL_EVENTS.TASK_START, + flags.refreshData ? gettext('Refetching latest results...') : gettext('Waiting for the query to complete...'), + startTime + ); this.setStartTime(startTime); this.query = query; this.historyQuerySource = flags.isQueryTool ? QuerySources.EXECUTE : QuerySources.VIEW_DATA; - if(explainObject) { + if(flags.refreshData) { + this.historyQuerySource = null; + } else if(explainObject) { if(explainObject.analyze) { this.historyQuerySource = QuerySources.EXPLAIN_ANALYZE; } else { @@ -301,7 +306,7 @@ export class ResultSetUtils { e, { connectionLostCallback: ()=>{ - this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, '', flags.external, true, flags.executeCursor); + this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, {explainObject, external: flags.external, reconnect: true, executeCursor: flags.executeCursor}); }, checkTransaction: true, } @@ -357,7 +362,7 @@ export class ResultSetUtils { }); this.eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, { connectionLostCallback: ()=>{ - this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, this.query, explainObject, '', flags.external, true, flags.executeCursor); + this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, this.query, {explainObject, external: flags.external, reconnect: true, executeCursor: flags.executeCursor}); }, checkTransaction: true, }); @@ -396,7 +401,7 @@ export class ResultSetUtils { if(this.qtPref?.query_success_notification) { pgAdmin.Browser.notifier.success(msg); } - if(!ResultSetUtils.isQueryStillRunning(httpMessage)) { + if(!ResultSetUtils.isQueryStillRunning(httpMessage) && this.historyQuerySource) { this.eventBus.fireEvent(QUERY_TOOL_EVENTS.PUSH_HISTORY, { status: true, start_time: this.startTime, @@ -414,16 +419,12 @@ export class ResultSetUtils { } } - getMoreRows(all=false) { - let url = url_for('sqleditor.fetch', { + getWindowRows(fromRownum, toRownum) { + let url = url_for('sqleditor.fetch_window', { 'trans_id': this.transId, + 'from_rownum': fromRownum, + 'to_rownum': toRownum, }); - if(all) { - url = url_for('sqleditor.fetch_all', { - 'trans_id': this.transId, - 'fetch_all': 1, - }); - } return this.api.get(url); } @@ -791,6 +792,11 @@ function dataChangeReducer(state, action) { ...dataChange.deleted, ...action.add, }; + if(action.all) { + dataChange.delete_all = true; + } else { + dataChange.delete_all = false; + } break; case 'reset': dataChange = { @@ -798,6 +804,7 @@ function dataChangeReducer(state, action) { added: {}, added_index: {}, deleted: {}, + delete_all: false, }; break; default: @@ -818,12 +825,14 @@ export function ResultSet() { const [queryData, setQueryData] = useState(null); const [rows, setRows] = useState([]); const [columns, setColumns] = useState([]); - const [isLoadingMore, setIsLoadingMore] = useState(false); const api = getApiInstance(); const rsu = React.useRef(new ResultSetUtils(api, queryToolCtx, queryToolCtx.params.trans_id, queryToolCtx.params.is_query_tool)); const [dataChangeStore, dispatchDataChange] = React.useReducer(dataChangeReducer, {}); const [selectedRows, setSelectedRows] = useState(new Set()); const [selectedColumns, setSelectedColumns] = useState(new Set()); + // NONE - no select, PAGE - show select all, ALL - select all. + const [allRowsSelect, setAllRowsSelect] = useState('NONE'); + const selectedCell = useRef([]); const selectedRange = useRef(null); const setSelectedCell = (val)=>{ @@ -855,7 +864,9 @@ export function ResultSet() { eventBus.fireEvent(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CELL_CHANGED, selectedRows.size, selectedColumns.size, selectedRange.current, selectedCell.current?.length); }; - const executionStartCallback = async (query, explainObject, macroSQL, external=false, reconnect=false, executeCursor=false)=>{ + const executionStartCallback = async (query, { + explainObject, macroSQL, external=false, reconnect=false, executeCursor=false, refreshData=false + })=>{ const yesCallback = async ()=>{ /* Reset */ eventBus.fireEvent(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, null); @@ -871,7 +882,7 @@ export function ResultSet() { setColumns([]); setRows([]); }, - {isQueryTool: queryToolCtx.params.is_query_tool, external: external, reconnect: reconnect, executeCursor: executeCursor} + {isQueryTool: queryToolCtx.params.is_query_tool, external: external, reconnect: reconnect, executeCursor: executeCursor, refreshData: refreshData} ); }; @@ -916,7 +927,7 @@ export function ResultSet() { }); }; - if(isDataChanged()) { + if(isDataChanged() && !refreshData) { queryToolCtx.modal.confirm( gettext('Unsaved changes'), gettext('The data has been modified, but not saved. Are you sure you wish to discard the changes?'), @@ -1010,19 +1021,41 @@ export function ResultSet() { eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_INCLUDE_EXCLUDE_FILTER, triggerFilter); eventBus.registerListener(QUERY_TOOL_EVENTS.GOTO_LAST_SCROLL, triggerResetScroll); + + eventBus.registerListener(QUERY_TOOL_EVENTS.ALL_PAGE_ROWS_SELECTED, (selectAll)=>{ + if(selectAll) { + setAllRowsSelect('PAGE'); + } else { + setAllRowsSelect('NONE'); + } + }); + + eventBus.registerListener(QUERY_TOOL_EVENTS.ALL_ROWS_SELECTED, ()=>{ + setAllRowsSelect('ALL'); + }); + + eventBus.registerListener(QUERY_TOOL_EVENTS.CLEAR_ROWS_SELECTED, ()=>{ + setSelectedRows(new Set()); + setAllRowsSelect('NONE'); + }); }, []); useEffect(()=>{ - eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_START, executionStartCallback); + const deregExec = eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_START, executionStartCallback); return ()=>{ - eventBus.deregisterListener(QUERY_TOOL_EVENTS.EXECUTION_START, executionStartCallback); + deregExec(); }; - }, [dataChangeStore]); + }, [dataChangeStore, dataOutputQuery]); useEffect(()=>{ fireRowsColsCellChanged(); + setAllRowsSelect('NONE'); }, [selectedRows.size, selectedColumns.size]); + useEffect(()=>{ + eventBus.fireEvent(QUERY_TOOL_EVENTS.ALL_ROWS_SELECTED_STATUS, allRowsSelect); + }, [allRowsSelect]); + useEffect(()=>{ rsu.current.transId = queryToolCtx.params.trans_id; }, [queryToolCtx.params.trans_id]); @@ -1031,45 +1064,44 @@ export function ResultSet() { eventBus.fireEvent(QUERY_TOOL_EVENTS.RESET_GRAPH_VISUALISER, columns); }, [columns]); - const fetchMoreRows = async (all=false, callback=undefined)=>{ - if(queryData.has_more_rows) { - let res = []; - setIsLoadingMore(true); - try { - res = await rsu.current.getMoreRows(all); - const newRows = rsu.current.processRows(res.data.data.result, columns); - setRows((prevRows)=>[...prevRows, ...newRows]); - setQueryData((prev)=>({ - ...prev, - has_more_rows: res.data.data.has_more_rows, - rows_fetched_to: res.data.data.rows_fetched_to!=0 ? res.data.data.rows_fetched_to : prev.rows_fetched_to, - })); - } catch (e) { - eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, - e, - { - connectionLostCallback: ()=>{ - eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, null, '', false, true); - }, - checkTransaction: true, - } - ); - } finally { - setIsLoadingMore(false); - } + const fetchWindow = async (fromRownum, toRownum, callback)=>{ + let res = []; + setLoaderText(gettext('Fetching rows...')); + try { + res = await rsu.current.getWindowRows(fromRownum, toRownum); + const newRows = rsu.current.processRows(res.data.data.result, columns); + setRows([...newRows]); + setQueryData((prev)=>({ + ...prev, + pagination: res.data.data.pagination, + rows_fetched_to: res.data.data.rows_fetched_to!=0 ? res.data.data.rows_fetched_to : prev.rows_fetched_to, + })); + } catch (e) { + eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, + e, + { + connectionLostCallback: ()=>{ + eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, {external: false, reconnect: true}); + }, + checkTransaction: true, + } + ); + } finally { + setLoaderText(''); } callback?.(); }; - useEffect(()=>{ - eventBus.registerListener(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, fetchMoreRows); - return ()=>{ - eventBus.deregisterListener(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, fetchMoreRows); - }; - }, [queryData?.has_more_rows, columns]); useEffect(()=>{ - eventBus.fireEvent(QUERY_TOOL_EVENTS.ROWS_FETCHED, queryData?.rows_fetched_to, queryData?.rows_affected); - }, [queryData?.rows_fetched_to, queryData?.rows_affected]); + eventBus.registerListener(QUERY_TOOL_EVENTS.FETCH_WINDOW, fetchWindow); + return ()=>{ + eventBus.deregisterListener(QUERY_TOOL_EVENTS.FETCH_WINDOW, fetchWindow); + }; + }, [columns]); + + useEffect(()=>{ + eventBus.fireEvent(QUERY_TOOL_EVENTS.TOTAL_ROWS_COUNT, queryData?.rows_affected); + }, [queryData?.rows_affected]); const warnSaveDataClose = ()=>{ // No changes. @@ -1116,6 +1148,7 @@ export function ResultSet() { let {data: respData} = await rsu.current.saveData({ updated: dataChangeStore.updated, deleted: dataChangeStore.deleted, + delete_all: dataChangeStore.delete_all, added_index: dataChangeStore.added_index, added: added, columns: columns, @@ -1152,37 +1185,6 @@ export function ResultSet() { } eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_DONE, true); - if(_.size(dataChangeStore.added)) { - // Update the rows in a grid after addition - respData.data.query_results.forEach((qr)=>{ - if(!_.isNull(qr.row_added)) { - let rowClientPK = Object.keys(qr.row_added)[0]; - setRows((prevRows)=>{ - let rowIdx = prevRows.findIndex((r)=>rowKeyGetter(r)==rowClientPK); - return [ - ...prevRows.slice(0, rowIdx), - { - ...prevRows[rowIdx], - ...qr.row_added[rowClientPK], - }, - ...prevRows.slice(rowIdx+1), - ]; - }); - } - }); - } - let deletedKeys = Object.keys(dataChangeStore.deleted); - if(deletedKeys.length == rows.length) { - setRows([]); - } - else if(deletedKeys.length > 0) { - setRows((prevRows)=>{ - return prevRows.filter((row)=>{ - return deletedKeys.indexOf(row[rsu.current.clientPK]) == -1; - }); - }); - setColumns((prev)=>prev); - } dispatchDataChange({type: 'reset'}); setSelectedRows(new Set()); setSelectedColumns(new Set()); @@ -1192,6 +1194,8 @@ export function ResultSet() { if(respData.data.transaction_status > CONNECTION_STATUS.TRANSACTION_STATUS_IDLE) { pgAdmin.Browser.notifier.info(gettext('Auto-commit is off. You still need to commit changes to the database.')); } + + eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, {refreshData: true}); } catch (error) { eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_DONE, false); eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, { @@ -1292,6 +1296,7 @@ export function ResultSet() { type: 'deleted', add: add, remove: remove, + all: remove.length > 0 ? false : allRowsSelect == 'ALL', }); }; @@ -1307,7 +1312,7 @@ export function ResultSet() { return ()=>{ eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_DELETE_ROWS, triggerDeleteRows); }; - }, [selectedRows, queryData, dataChangeStore, rows]); + }, [selectedRows, queryData, dataChangeStore, rows, allRowsSelect]); useEffect(()=>{ const triggerAddRows = (_rows, fromClipboard, pasteSerials)=>{ @@ -1317,7 +1322,7 @@ export function ResultSet() { selectedRowsSorted.sort(); insPosn = _.findIndex(rows, (r)=>rowKeyGetter(r)==selectedRowsSorted[selectedRowsSorted.length-1])+1; } - let byteaCellSelection = columns.filter(o=>o.type=='bytea'); + let byteaCellSelection = columns.filter(o=>o.type=='bytea'); if (byteaCellSelection.length>0) { _rows = _rows.map(x=>{ byteaCellSelection.forEach(r=>{ @@ -1373,20 +1378,6 @@ export function ResultSet() { return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries); }, [rows, columns, selectedRows.size, selectedColumns.size]); - const handleScroll = (e) => { - // Set scroll current position of RestSet. - if (!_.isNull(e.currentTarget) && isResettingScroll.current) { - lastScrollRef.current = { - ref: { ...e }, - top: e.currentTarget.scrollTop, - left: e.currentTarget.scrollLeft - }; - } - - if (isLoadingMore || !rsu.current.isAtBottom(e)) return; - eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS); - }; - const triggerResetScroll = () => { // Reset the scroll position to previously saved location. if (lastScrollRef.current) { @@ -1463,17 +1454,20 @@ export function ResultSet() { return ( - {!queryData && } {queryData && <> - + ({ padding: '2px', display: 'flex', alignItems: 'center', - gap: '4px', + flexWrap: 'wrap', + rowGap: '4px', backgroundColor: theme.otherVars.editorToolbarBg, + justifyContent: 'space-between', ...theme.mixins.panelBorder.bottom, + + '& .PaginationInputs': { + display: 'flex', + alignItems: 'center', + gap: '4px', + + '& .PaginationInputs-divider': { + ...theme.mixins.panelBorder.right, + } + } })); const StyledEditor = styled('div')(({theme})=>({ @@ -76,7 +98,134 @@ ShowDataOutputQueryPopup.propTypes = { query: PropTypes.string, }; -export function ResultSetToolbar({query,canEdit, totalRowCount}) { + +function PaginationInputs({pagination, totalRowCount, clearSelection}) { + const eventBus = useContext(QueryToolEventsContext); + const [editPageRange, setEditPageRange] = useState(false); + const [errorInputs, setErrorInputs] = useState({ + 'from': false, + 'to': false, + 'pageNo': false + }); + const [inputs, setInputs] = useState({ + from: pagination.rows_from ?? 0, + to: pagination.rows_to ?? 0, + pageNo: pagination.page_no ?? 0, + }); + + const goToPage = (pageNo)=>{ + const from = (pageNo-1) * pagination.page_size + 1; + const to = from + pagination.page_size - 1; + eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_WINDOW, from, to); + clearSelection(); + }; + + const onInputChange = (key, value)=>{ + setInputs((prev)=>({...prev, [key]: value})); + }; + + const onInputKeydown = (e)=>{ + if(e.code === 'Enter' && !errorInputs.from && !errorInputs.to) { + e.preventDefault(); + eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_WINDOW, inputs.from, inputs.to); + } + }; + + const onInputKeydownPageNo = (e)=>{ + if(e.code === 'Enter' && !errorInputs.pageNo) { + e.preventDefault(); + goToPage(inputs.pageNo); + } + }; + + useEffect(()=>{ + // validate + setErrorInputs((prev)=>{ + let errors = {...prev}; + + if(minMaxValidator('', inputs.pageNo, 1, pagination.page_count)) { + errors.pageNo = true; + } else { + errors.pageNo = false; + } + if(minMaxValidator('', inputs.from, 1, inputs.to)) { + errors.from = true; + } else { + errors.from = false; + } + if(minMaxValidator('', inputs.to, 1, totalRowCount)) { + errors.to = true; + } else { + errors.to = false; + } + + return errors; + }); + }, [inputs, pagination]); + + return ( + + {editPageRange ? + +
{gettext('Showing rows:')}
+ onInputChange('from', value)} + onKeyDown={onInputKeydown} + error={errorInputs['from']} + /> +
{gettext('to')}
+ onInputChange('to', value)} + onKeyDown={onInputKeydown} + error={errorInputs['to']} + /> +
: {gettext('Showing rows: %s to %s', inputs.from, inputs.to)}} + + {editPageRange && eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_WINDOW, inputs.from, inputs.to)} + icon={} + />} + setEditPageRange((prev)=>!prev)} + icon={editPageRange ? : } + /> + +
 
+ {gettext('Page No:')} + onInputChange('pageNo', value)} + onKeyDown={onInputKeydownPageNo} + error={errorInputs['pageNo']} + /> + {gettext('of')} {pagination.page_count} +
 
+ + goToPage(1)} icon={}/> + goToPage(pagination.page_no-1)} icon={}/> + goToPage(pagination.page_no+1)} icon={}/> + goToPage(pagination.page_count)} icon={} /> + +
+ ); +} +export function ResultSetToolbar({query, canEdit, totalRowCount, pagination, allRowsSelect}) { const eventBus = useContext(QueryToolEventsContext); const queryToolCtx = useContext(QueryToolContext); const [dataOutputQueryBtn,setDataOutputQueryBtn] = useState(false); @@ -208,45 +357,74 @@ export function ResultSetToolbar({query,canEdit, totalRowCount}) { }, ], queryToolCtx.mainContainerRef); + const clearSelection = ()=>{ + eventBus.fireEvent(QUERY_TOOL_EVENTS.CLEAR_ROWS_SELECTED); + }; + return ( <> - - } - shortcut={queryToolPref.btn_add_row} disabled={!canEdit} onClick={addRow} /> - } - shortcut={FIXED_PREF.copy} disabled={buttonsDisabled['copy-rows']} onClick={copyData} /> - } splitButton - name="menu-copyheader" ref={copyMenuRef} onClick={openMenu} /> - } - shortcut={queryToolPref.btn_paste_row} disabled={!canEdit} onClick={pasteRows} /> - } splitButton - name="menu-pasteoptions" ref={pasetMenuRef} onClick={openMenu} /> - } - shortcut={queryToolPref.btn_delete_row} disabled={buttonsDisabled['delete-rows'] || !canEdit} onClick={deleteRows} /> - - - } - shortcut={queryToolPref.save_data} disabled={buttonsDisabled['save-data'] || !canEdit} onClick={saveData}/> - - - } - onClick={downloadResult} shortcut={queryToolPref.download_results} - disabled={buttonsDisabled['save-result']} /> - - - } - onClick={showGraphVisualiser} disabled={buttonsDisabled['save-result']} /> - - {query && - <> + - } - onClick={()=>{setDataOutputQueryBtn(prev=>!prev);}} onBlur={()=>{setDataOutputQueryBtn(false);}} disabled={!query} id='sql-query'/> + } + shortcut={queryToolPref.btn_add_row} disabled={!canEdit} onClick={addRow} /> + } + shortcut={FIXED_PREF.copy} disabled={buttonsDisabled['copy-rows']||allRowsSelect=='ALL'} onClick={copyData} /> + } splitButton + name="menu-copyheader" ref={copyMenuRef} onClick={openMenu} /> + } + shortcut={queryToolPref.btn_paste_row} disabled={!canEdit} onClick={pasteRows} /> + } splitButton + name="menu-pasteoptions" ref={pasetMenuRef} onClick={openMenu} /> + } + shortcut={queryToolPref.btn_delete_row} disabled={buttonsDisabled['delete-rows'] || !canEdit} onClick={deleteRows} /> - { dataOutputQueryBtn && } - - } + + } + shortcut={queryToolPref.save_data} disabled={buttonsDisabled['save-data'] || !canEdit} onClick={saveData}/> + + + } + onClick={downloadResult} shortcut={queryToolPref.download_results} + disabled={buttonsDisabled['save-result']} /> + + + } + onClick={showGraphVisualiser} disabled={buttonsDisabled['save-result']} /> + + {query && + <> + + } + onClick={()=>{setDataOutputQueryBtn(prev=>!prev);}} onBlur={()=>{setDataOutputQueryBtn(false);}} disabled={!query} id='sql-query'/> + + { dataOutputQueryBtn && } + + } + { + allRowsSelect == 'PAGE' && ( +
+ {gettext('All rows on this page are selected.')} + + eventBus.fireEvent(QUERY_TOOL_EVENTS.ALL_ROWS_SELECTED)}>Select All {totalRowCount} Rows + +
+ ) + } + { + allRowsSelect == 'ALL' && ( +
+ {gettext('All %s rows are selected.', totalRowCount)} + + {gettext('Clear Selection')} + +
+ ) + } +
+ + +
{ eventBus.registerListener(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, (newPos)=>{ @@ -70,20 +72,30 @@ export function StatusBar({eol, handleEndOfLineChange}) { pauseTimer(endTime); setLastTaskText(taskText); }); - eventBus.registerListener(QUERY_TOOL_EVENTS.ROWS_FETCHED, (fetched, total)=>{ - setRowsCount([fetched||0, total||0]); + eventBus.registerListener(QUERY_TOOL_EVENTS.TOTAL_ROWS_COUNT, (total)=>{ + setRowsCount(total); + }); + eventBus.registerListener(QUERY_TOOL_EVENTS.ALL_ROWS_SELECTED_STATUS, (v)=>{ + setAllRowsSelect(v); }); eventBus.registerListener(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CELL_CHANGED, (rows)=>{ setSelectedRowsCount(rows); }); - eventBus.registerListener(QUERY_TOOL_EVENTS.DATAGRID_CHANGED, (_isDirty, dataChangeStore)=>{ + }, []); + + useEffect(()=>{ + const unregDataChange = eventBus.registerListener(QUERY_TOOL_EVENTS.DATAGRID_CHANGED, (_isDirty, dataChangeStore)=>{ setDataRowChangeCounts({ added: Object.keys(dataChangeStore.added||{}).length, updated: Object.keys(dataChangeStore.updated||{}).length, - deleted: Object.keys(dataChangeStore.deleted||{}).length, + deleted: dataChangeStore.delete_all ? rowsCount : Object.keys(dataChangeStore.deleted||{}).length, }); }); - }, []); + + return ()=>{ + unregDataChange(); + }; + }, [rowsCount]); let stagedText = ''; if(dataRowChangeCounts.added > 0) { @@ -98,7 +110,7 @@ export function StatusBar({eol, handleEndOfLineChange}) { return ( - {gettext('Total rows: %s of %s', rowsCount[0], rowsCount[1])} + {rowsCount && {gettext('Total rows: %s', rowsCount)}} {lastTaskText && {lastTaskText} {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}.{msec.toString().padStart(3, '0')} } @@ -106,13 +118,13 @@ export function StatusBar({eol, handleEndOfLineChange}) { {lastTaskText} {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}.{msec.toString().padStart(3, '0')} } {Boolean(selectedRowsCount) && - {gettext('Rows selected: %s',selectedRowsCount)}} + {gettext('Rows selected: %s', allRowsSelect == 'ALL' ? rowsCount : selectedRowsCount)}} {stagedText && {gettext('Changes staged: %s', stagedText)} } - + diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/delete.sql b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/delete.sql index 25ddf6487..1f3b497f9 100644 --- a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/delete.sql +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/delete.sql @@ -6,8 +6,8 @@ DELETE FROM {{ conn|qtIdent(nsp_name, object_name) }} {% elif no_of_keys > 1 %} WHERE ({% for each_label in primary_key_labels %}{{ conn|qtIdent(each_label) }}{% if not loop.last %}, {% endif %}{% endfor %}) IN {% endif %} -{### Rows to delete ###} +{% if no_of_keys >= 1 %}{### Rows to delete ###} ({% for obj in data %}{% if no_of_keys == 1 %}{{ obj[primary_key_labels[0]]|qtLiteral(conn) }}{% elif no_of_keys > 1 %} {### Here we need to make tuple for each row ###} ({% for each_label in primary_key_labels %}{{ obj[each_label]|qtLiteral(conn) }}{% if not loop.last %}, {% endif %}{% endfor %}){% endif %}{% if not loop.last %}, {% endif %} -{% endfor %}); +{% endfor %}){% endif %}; diff --git a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py index 354a36ad5..29c70d19d 100644 --- a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py +++ b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py @@ -14,7 +14,7 @@ from pgadmin.utils.constants import PREF_LABEL_DISPLAY,\ PREF_LABEL_EDITOR, PREF_LABEL_CSV_TXT, PREF_LABEL_RESULTS_GRID,\ PREF_LABEL_SQL_FORMATTING, PREF_LABEL_GRAPH_VISUALISER from pgadmin.utils import SHORTCUT_FIELDS as shortcut_fields -from config import ON_DEMAND_RECORD_COUNT +from config import DATA_RESULT_ROWS_PER_PAGE UPPER_CASE_STR = gettext('Upper case') LOWER_CASE_STR = gettext('Lower case') @@ -346,15 +346,15 @@ def register_query_tool_preferences(self): ), ) - self.on_demand_record_count = self.preference.register( - 'Results_grid', 'on_demand_record_count', - gettext("On demand record count"), 'integer', ON_DEMAND_RECORD_COUNT, - min_val=1, + self.data_result_rows_per_page = self.preference.register( + 'Results_grid', 'data_result_rows_per_page', + gettext("Data result rows per page"), 'integer', + DATA_RESULT_ROWS_PER_PAGE, min_val=10, category_label=PREF_LABEL_RESULTS_GRID, - help_str=gettext('Specify the number of records to fetch in one batch ' - 'in query tool when query result set is large. ' - 'Changing this value will override ' - 'ON_DEMAND_RECORD_COUNT setting from config file.') + help_str=gettext('Specify the number of records to fetch in one batch.' + ' Changing this value will override' + ' DATA_RESULT_ROWS_PER_PAGE setting from config ' + ' file.') ) self.sql_font_size = self.preference.register( diff --git a/web/pgadmin/tools/sqleditor/utils/save_changed_data.py b/web/pgadmin/tools/sqleditor/utils/save_changed_data.py index a72dc2e22..86b99afef 100644 --- a/web/pgadmin/tools/sqleditor/utils/save_changed_data.py +++ b/web/pgadmin/tools/sqleditor/utils/save_changed_data.py @@ -188,35 +188,37 @@ def save_changed_data(changed_data, columns_info, conn, command_obj, # For deleted rows elif of_type == 'deleted': + delete_all = changed_data.get('delete_all', False) list_of_sql[of_type] = [] is_first = True rows_to_delete = [] - keys = None - no_of_keys = None - for each_row in changed_data[of_type]: - rows_to_delete.append(changed_data[of_type][each_row]) - # Fetch the keys for SQL generation - if is_first: - # We need to covert dict_keys to normal list in - # Python3 - # In Python2, it's already a list & We will also - # fetch column names using index - keys = list( - changed_data[of_type][each_row].keys() - ) - no_of_keys = len(keys) - is_first = False - # Map index with column name for each row - for row in rows_to_delete: - for k, v in row.items(): - # Set primary key with label & delete index based - # mapped key - try: - row[changed_data['columns'] - [int(k)]['name']] = v - except ValueError: - continue - del row[k] + keys = [] + no_of_keys = 0 + if not delete_all: + for each_row in changed_data[of_type]: + rows_to_delete.append(changed_data[of_type][each_row]) + # Fetch the keys for SQL generation + if is_first: + # We need to covert dict_keys to normal list in + # Python3 + # In Python2, it's already a list & We will also + # fetch column names using index + keys = list( + changed_data[of_type][each_row].keys() + ) + no_of_keys = len(keys) + is_first = False + # Map index with column name for each row + for row in rows_to_delete: + for k, v in row.items(): + # Set primary key with label & delete index based + # mapped key + try: + row[changed_data['columns'] + [int(k)]['name']] = v + except ValueError: + continue + del row[k] sql = render_template( "/".join([command_obj.sql_path, 'delete.sql']), diff --git a/web/pgadmin/utils/driver/psycopg3/connection.py b/web/pgadmin/utils/driver/psycopg3/connection.py index 2e66b6039..739ae26e0 100644 --- a/web/pgadmin/utils/driver/psycopg3/connection.py +++ b/web/pgadmin/utils/driver/psycopg3/connection.py @@ -1308,6 +1308,7 @@ WHERE db.datname = current_database()""") return True, {'columns': columns, 'rows': rows} def async_fetchmany_2darray(self, records=2000, + from_rownum=0, to_rownum=0, formatted_exception_msg=False): """ User should poll and check if status is ASYNC_OK before calling this @@ -1342,6 +1343,10 @@ WHERE db.datname = current_database()""") try: if records == -1: result = cur.fetchall(_tupples=True) + elif records is None: + result = cur.fetchwindow(from_rownum=from_rownum, + to_rownum=to_rownum, + _tupples=True) else: result = cur.fetchmany(records, _tupples=True) except psycopg.ProgrammingError: @@ -1538,6 +1543,12 @@ Failed to reset the connection to the server due to following error: return self.row_count + @property + def total_rows(self): + if self.__async_cursor is None: + return 0 + return self.__async_cursor.rowcount + def get_column_info(self): """ This function will returns list of columns for last async sql command diff --git a/web/pgadmin/utils/driver/psycopg3/cursor.py b/web/pgadmin/utils/driver/psycopg3/cursor.py index bb3e6da66..4a3c12995 100644 --- a/web/pgadmin/utils/driver/psycopg3/cursor.py +++ b/web/pgadmin/utils/driver/psycopg3/cursor.py @@ -364,6 +364,20 @@ class AsyncDictCursor(_async_cursor): self.row_factory = dict_row return res + def fetchwindow(self, from_rownum=0, to_rownum=0, _tupples=False): + """ + Fetch many tuples as ordered dictionary list. + """ + self._odt_desc = None + self.row_factory = tuple_row + asyncio.run(self._scrollcur(from_rownum, "absolute")) + res = asyncio.run(self._fetchmany(to_rownum - from_rownum + 1)) + if not _tupples and res is not None: + res = [self._dict_tuple(t) for t in res] + + self.row_factory = dict_row + return res + async def _scrollcur(self, position, mode): """ Fetch all tuples as ordered dictionary list. diff --git a/web/regression/feature_tests/query_tool_tests.py b/web/regression/feature_tests/query_tool_tests.py index e6a0d7557..2f14f809d 100644 --- a/web/regression/feature_tests/query_tool_tests.py +++ b/web/regression/feature_tests/query_tool_tests.py @@ -47,10 +47,10 @@ class QueryToolFeatureTest(BaseFeatureTest): def runTest(self): self._reset_options() - # on demand result set on scrolling. - print("\nOn demand query result... ", + # pagination result on page change. + print("\nPagination query result... ", file=sys.stderr, end="") - self._on_demand_result() + self._pagination_result() self.page.clear_query_tool() # explain query with verbose and cost @@ -129,18 +129,12 @@ class QueryToolFeatureTest(BaseFeatureTest): # close menu query_op.click() - def _on_demand_result(self): - ON_DEMAND_CHUNKS = 2 - row_id_to_find = config.ON_DEMAND_RECORD_COUNT * ON_DEMAND_CHUNKS - - query = """-- On demand query result on scroll --- Grid select all --- Column select all + def _pagination_result(self): + query = """-- Pagination result SELECT generate_series(1, {}) as id1, 'dummy' as id2""".format( - config.ON_DEMAND_RECORD_COUNT * ON_DEMAND_CHUNKS) + config.DATA_RESULT_ROWS_PER_PAGE * 2.5) - print("\nOn demand result set on scrolling... ", - file=sys.stderr, end="") + print("\nPagination result... ", file=sys.stderr, end="") self.page.execute_query(query) # wait for header of the table to be visible @@ -151,94 +145,33 @@ SELECT generate_series(1, {}) as id1, 'dummy' as id2""".format( (By.CSS_SELECTOR, QueryToolLocators.query_output_cells))) - self.page.find_by_css_selector( - QueryToolLocators.query_output_canvas_css) + for i, page in enumerate([ + {'page_info': '1 to 1000', 'cell_rownum': '1'}, + {'page_info': '1001 to 2000', 'cell_rownum': '1001'}, + {'page_info': '2001 to 2500', 'cell_rownum': '2001'} + ]): + page_info = self.page.find_by_css_selector( + QueryToolLocators.pagination_inputs + + f' span:nth-of-type(1)') - self._check_ondemand_result(row_id_to_find) - print("OK.", file=sys.stderr) + self.assertEqual(page_info.text, f"Showing: {page['page_info']}") - print("On demand result set on grid select all... ", - file=sys.stderr, end="") - self.page.click_execute_query_button() + page_info = self.page.find_by_css_selector( + QueryToolLocators.pagination_inputs + ' span:nth-of-type(3)') - # wait for header of the table to be visible - self.page.find_by_css_selector( - QueryToolLocators.query_output_canvas_css) + self.assertEqual(page_info.text, "of 3") - # wait for the rows in the table to be displayed - self.wait.until(EC.presence_of_element_located( - (By.CSS_SELECTOR, - QueryToolLocators.query_output_cells)) - ) + cell_rownum = self.page.find_by_css_selector( + QueryToolLocators.query_output_cells + ':nth-of-type(1)') - # Select all rows in a table - multiple_check = True - while multiple_check: - try: - select_all = self.wait.until(EC.element_to_be_clickable( - (By.XPATH, QueryToolLocators.select_all_column))) - select_all.click() - multiple_check = False - except (StaleElementReferenceException, - ElementClickInterceptedException): - pass + self.assertEqual(cell_rownum.text, page['cell_rownum']) - self._check_ondemand_result(row_id_to_find) - print("OK.", file=sys.stderr) - - print("On demand result set on column select all... ", - file=sys.stderr, end="") - self.page.click_execute_query_button() - - self.page.wait_for_query_tool_loading_indicator_to_disappear() - - # wait for header of the table to be visible - self.wait.until(EC.visibility_of_element_located( - (By.CSS_SELECTOR, QueryToolLocators.query_output_canvas_css))) - - # wait for the rows in the table to be displayed - self.wait.until(EC.presence_of_element_located( - (By.CSS_SELECTOR, - QueryToolLocators.query_output_cells)) - ) - - self.wait.until(EC.presence_of_element_located( - (By.CSS_SELECTOR, QueryToolLocators.query_output_canvas_css))) - - self._check_ondemand_result(row_id_to_find) - print("OK.", file=sys.stderr) - - def _check_ondemand_result(self, row_id_to_find): - # scroll to bottom to bring last row of next chunk in viewport. - scroll = 10 - status = False - while scroll: - # click on first data column to select all column. - column_1 = \ + if i < 2: self.page.find_by_css_selector( - QueryToolLocators.output_column_header_css.format('id1')) - column_1.click() - grid = self.page.find_by_css_selector('.rdg') - scrolling_height = grid.size['height'] - self.driver.execute_script( - "document.querySelector('.rdg').scrollTop=" - "document.querySelector('.rdg').scrollHeight" - ) - # Table height takes some time to update, for which their is no - # particular way - time.sleep(2) - if grid.size['height'] == scrolling_height and \ - self.page.check_if_element_exist_by_xpath( - QueryToolLocators.output_column_data_xpath.format( - row_id_to_find)): - status = True - break - else: - scroll -= 1 + QueryToolLocators.pagination_inputs + + ' button[aria-label="Next Page"]').click() - self.assertTrue( - status, "Element is not loaded to the rows id: " - "{}".format(row_id_to_find)) + self.page.wait_for_query_tool_loading_indicator_to_disappear() def _query_tool_explain_with_verbose_and_cost(self): query = """-- Explain query with verbose and cost diff --git a/web/regression/feature_tests/view_data_dml_queries.py b/web/regression/feature_tests/view_data_dml_queries.py index f05d4256f..aafbba5da 100644 --- a/web/regression/feature_tests/view_data_dml_queries.py +++ b/web/regression/feature_tests/view_data_dml_queries.py @@ -172,8 +172,6 @@ CREATE TABLE public.nonintpkey self._copy_paste_row(config_data_local) self._update_row(config_data_local) - self.page.click_tab("Messages") - self._verify_messsages("") self.page.click_tab("Data Output") updated_row_data = { i: config_data_local['update'][i] if i in config_data_local[ diff --git a/web/regression/feature_utils/locators.py b/web/regression/feature_utils/locators.py index ee846b8cd..08ba47f2d 100644 --- a/web/regression/feature_utils/locators.py +++ b/web/regression/feature_utils/locators.py @@ -232,6 +232,8 @@ class QueryToolLocators: query_output_canvas_css = "#id-dataoutput .rdg" + pagination_inputs = "#id-dataoutput .PaginationInputs" + query_output_cells = ".rdg-cell[role='gridcell']" sql_editor_message = "//div[@id='id-messages'][contains(string(), '{}')]"