From 2c0e74173d5094cb5d3b2f43d5e931e85467ddbc Mon Sep 17 00:00:00 2001 From: "me.gorkov" Date: Tue, 10 Mar 2026 12:16:13 +0300 Subject: [PATCH 1/6] Add integration with external Explain PostgreSQL API for query plans analysis Added a new module to pgAdmin 4 that allows users to analyze query plans. The module sends the plan to an external API service for visualization and detailed breakdown. --- web/pgadmin/messages.pot | 171 +- .../js/components/ReactCodeMirror/index.jsx | 16 +- web/pgadmin/tools/__init__.py | 3 + web/pgadmin/tools/ep/__init__.py | 185 + .../static/js/ExplainPostgreSQL/formatSQL.js | 32 + .../ep/static/js/ExplainPostgreSQL/index.jsx | 91 + .../js/components/QueryToolConstants.js | 2 + .../js/components/sections/ResultSet.jsx | 17 + .../translations/cs/LC_MESSAGES/messages.mo | Bin 323138 -> 303713 bytes .../translations/cs/LC_MESSAGES/messages.po | 2262 ++++++------ .../translations/de/LC_MESSAGES/messages.mo | Bin 65350 -> 65350 bytes .../translations/de/LC_MESSAGES/messages.po | 171 +- .../translations/es/LC_MESSAGES/messages.mo | Bin 98101 -> 98101 bytes .../translations/es/LC_MESSAGES/messages.po | 171 +- .../translations/fr/LC_MESSAGES/messages.mo | Bin 252818 -> 252818 bytes .../translations/fr/LC_MESSAGES/messages.po | 171 +- .../translations/id/LC_MESSAGES/messages.mo | Bin 232102 -> 232102 bytes .../translations/id/LC_MESSAGES/messages.po | 171 +- .../translations/it/LC_MESSAGES/messages.mo | Bin 329933 -> 310505 bytes .../translations/it/LC_MESSAGES/messages.po | 3118 +++++++++++++---- .../translations/ja/LC_MESSAGES/messages.mo | Bin 298854 -> 298854 bytes .../translations/ja/LC_MESSAGES/messages.po | 171 +- .../translations/ko/LC_MESSAGES/messages.po | 171 +- .../translations/pl/LC_MESSAGES/messages.mo | Bin 81122 -> 81122 bytes .../translations/pl/LC_MESSAGES/messages.po | 171 +- .../pt_BR/LC_MESSAGES/messages.mo | Bin 133849 -> 133849 bytes .../pt_BR/LC_MESSAGES/messages.po | 171 +- .../translations/ru/LC_MESSAGES/messages.mo | Bin 227407 -> 228423 bytes .../translations/ru/LC_MESSAGES/messages.po | 173 +- .../translations/sv/LC_MESSAGES/messages.po | 3077 ++++++++-------- .../zh_Hans_CN/LC_MESSAGES/messages.mo | Bin 189866 -> 189866 bytes .../zh_Hans_CN/LC_MESSAGES/messages.po | 171 +- .../zh_Hant_TW/LC_MESSAGES/messages.mo | Bin 187204 -> 187204 bytes .../zh_Hant_TW/LC_MESSAGES/messages.po | 171 +- 34 files changed, 6964 insertions(+), 3893 deletions(-) create mode 100644 web/pgadmin/tools/ep/__init__.py create mode 100644 web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/formatSQL.js create mode 100644 web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/index.jsx diff --git a/web/pgadmin/messages.pot b/web/pgadmin/messages.pot index 4f09ff46579..049447c4950 100644 --- a/web/pgadmin/messages.pot +++ b/web/pgadmin/messages.pot @@ -8,14 +8,14 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2026-02-26 18:55+0530\n" +"POT-Creation-Date: 2026-03-10 11:16+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.17.0\n" +"Generated-By: Babel 2.18.0\n" #: pgadmin/__init__.py:350 pgadmin/authenticate/internal.py:26 msgid "Incorrect username or password." @@ -841,6 +841,7 @@ msgid "Toggle comment" msgstr "" #: pgadmin/browser/register_editor_preferences.py:109 +#: pgadmin/tools/ep/__init__.py:57 #: pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx:631 msgid "Format SQL" msgstr "" @@ -10983,6 +10984,8 @@ msgid "Activity" msgstr "" #: pgadmin/dashboard/static/js/Dashboard.jsx:319 +#: pgadmin/tools/ep/__init__.py:36 pgadmin/tools/ep/__init__.py:44 +#: pgadmin/tools/ep/__init__.py:51 pgadmin/tools/ep/__init__.py:58 msgid "Configuration" msgstr "" @@ -13461,7 +13464,7 @@ msgid "All Files" msgstr "" #: pgadmin/misc/file_manager/static/js/components/GridView.jsx:146 -#: pgadmin/misc/file_manager/static/js/components/ListView.jsx:171 +#: pgadmin/misc/file_manager/static/js/components/ListView.jsx:173 msgid "No files/folders found" msgstr "" @@ -13486,7 +13489,7 @@ msgstr "" msgid "The master password is not set." msgstr "" -#: pgadmin/misc/workspaces/__init__.py:53 pgadmin/tools/__init__.py:87 +#: pgadmin/misc/workspaces/__init__.py:53 pgadmin/tools/__init__.py:90 #: pgadmin/tools/schema_diff/__init__.py:117 #: pgadmin/tools/sqleditor/__init__.py:177 msgid "This URL cannot be requested directly." @@ -14264,6 +14267,7 @@ msgid "Heap Blocks" msgstr "" #: pgadmin/static/js/Explain/index.jsx:513 +#: pgadmin/tools/ep/static/js/ExplainPostgreSQL/index.jsx:28 msgid "" "Use the Explain/Explain Analyze button to generate the plan for a query. " "Alternatively, you can also execute \"EXPLAIN (FORMAT JSON) [QUERY]\"." @@ -14277,7 +14281,7 @@ msgstr "" msgid "Analysis" msgstr "" -#: pgadmin/static/js/PgTreeView/index.jsx:144 +#: pgadmin/static/js/PgTreeView/index.jsx:185 msgid "No objects are found to display" msgstr "" @@ -14531,12 +14535,12 @@ msgstr "" msgid "Accesskey" msgstr "" -#: pgadmin/static/js/components/ReactCodeMirror/index.jsx:51 +#: pgadmin/static/js/components/ReactCodeMirror/index.jsx:52 #: pgadmin/tools/sqleditor/static/js/components/sections/QueryHistory.jsx:316 msgid "Copied!" msgstr "" -#: pgadmin/static/js/components/ReactCodeMirror/index.jsx:51 +#: pgadmin/static/js/components/ReactCodeMirror/index.jsx:52 #: pgadmin/tools/sqleditor/static/js/components/sections/QueryHistory.jsx:308 #: pgadmin/tools/sqleditor/static/js/components/sections/QueryHistory.jsx:311 #: pgadmin/tools/sqleditor/static/js/components/sections/ResultSetToolbar.jsx:418 @@ -15434,6 +15438,43 @@ msgstr "" msgid "Debugger - " msgstr "" +#: pgadmin/tools/ep/__init__.py:26 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1030 +msgid "Explain PostgreSQL" +msgstr "" + +#: pgadmin/tools/ep/__init__.py:35 +msgid "Explain PostgreSQL API" +msgstr "" + +#: pgadmin/tools/ep/__init__.py:37 +msgid "Explain PostgreSQL API endpoint (e.g. https://explain-postgresql.com)" +msgstr "" + +#: pgadmin/tools/ep/__init__.py:43 +msgid "Private Plans" +msgstr "" + +#: pgadmin/tools/ep/__init__.py:45 +msgid "Hide plans from public access on Explain PostgreSQL" +msgstr "" + +#: pgadmin/tools/ep/__init__.py:50 +msgid "Explain Plan" +msgstr "" + +#: pgadmin/tools/ep/__init__.py:52 +msgid "Analyze query plan via Explain PostgreSQL API" +msgstr "" + +#: pgadmin/tools/ep/__init__.py:59 +msgid "Format SQL using Explain PostgreSQL API" +msgstr "" + +#: pgadmin/tools/ep/__init__.py:92 pgadmin/tools/ep/__init__.py:117 +msgid "Failed to post data to the Explain Postgresql API" +msgstr "" + #: pgadmin/tools/erd/__init__.py:50 msgid "ERD tool" msgstr "" @@ -15728,8 +15769,8 @@ msgstr "" #: pgadmin/tools/erd/static/js/erd_tool/components/MainToolBar.jsx:113 #: pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx:335 -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:964 -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1164 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:976 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1191 msgid "Unsaved changes" msgstr "" @@ -17129,27 +17170,27 @@ msgstr "" msgid "Sort/Filter options" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js:99 +#: pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js:100 #: pgadmin/tools/sqleditor/utils/constant_definition.py:28 msgid "The session is idle and there is no current transaction." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js:100 +#: pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js:101 #: pgadmin/tools/sqleditor/utils/constant_definition.py:29 msgid "A command is currently in progress." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js:101 +#: pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js:102 #: pgadmin/tools/sqleditor/utils/constant_definition.py:30 msgid "The session is idle in a valid transaction block." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js:102 +#: pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js:103 #: pgadmin/tools/sqleditor/utils/constant_definition.py:31 msgid "The session is idle in a failed transaction block." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js:103 +#: pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js:104 #: pgadmin/tools/sqleditor/utils/constant_definition.py:32 msgid "The connection with the server is bad." msgstr "" @@ -17230,52 +17271,64 @@ msgstr "" msgid "New query tool for current connection" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:102 +#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:104 #, python-format msgid "%s of %s geometries rendered." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:105 +#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:107 msgid "3D geometries not rendered." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:108 +#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:110 msgid "Unsupported geometries not rendered." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:124 +#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:126 msgid "Empty row." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:183 +#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:185 #, python-format msgid "Geometries with non-SRID %s not rendered." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:364 +#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:367 msgid "Empty" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:369 +#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:372 msgid "Street" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:376 +#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:379 msgid "Topography" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:387 +#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:390 msgid "Gray Style" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:398 +#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:401 msgid "Light Color" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:409 +#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:412 msgid "Dark Matter" msgstr "" +#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:454 +msgid "" +"Query complete. Use the Geometry Viewer button in the Data Output tab to " +"visualize results." +msgstr "" + +#: pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx:455 +msgid "" +"No spatial data found. At least one geometry or geography column is " +"required for visualization." +msgstr "" + #: pgadmin/tools/sqleditor/static/js/components/sections/GraphVisualiser.jsx:259 msgid "" msgstr "" @@ -17413,7 +17466,7 @@ msgid "Execute options" msgstr "" #: pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx:567 -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:928 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:940 #: pgadmin/utils/constants.py:25 msgid "Explain" msgstr "" @@ -17702,141 +17755,141 @@ msgstr "" msgid "Remove All" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:144 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:145 msgid "hr" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:145 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:146 msgid "min" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:146 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:147 msgid "secs" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:147 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:148 msgid "msec" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:201 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:202 msgid "Refetching latest results..." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:201 -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:901 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:202 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:913 msgid "Waiting for the query to complete..." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:364 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:365 msgid "Connection Error" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:367 #: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:368 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:369 msgid "Execution Cancelled!" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:370 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:371 msgid "Execution Cancelled" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:441 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:442 msgid "Server Connected." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:725 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:726 #: pgadmin/tools/sqleditor/static/js/components/sections/StatusBar.jsx:66 msgid "Query complete" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:728 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:729 #, python-format msgid "Query returned successfully in %s." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:734 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:735 #, python-format msgid "Successfully run. Total query runtime: %s." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:735 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:736 #, python-format msgid "%s rows affected." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:965 -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1165 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:977 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1192 msgid "" "The data has been modified, but not saved. Are you sure you wish to " "discard the changes?" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:980 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:992 msgid "Applying the new filter..." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1034 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1061 msgid "Downloading results..." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1036 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1063 #, python-format msgid "Downloading results(%s)..." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1042 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1069 msgid "Setting the limit on the result..." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1056 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1083 msgid "Removing the filter..." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1117 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1144 msgid "Fetching rows..." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1191 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1218 msgid "Save data changes?" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1194 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1221 msgid "The data has changed. Do you want to save changes?" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1220 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1247 msgid "Saving data..." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1247 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1274 msgid "This query was generated by pgAdmin as part of a \"Save Data\" operation" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1259 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1286 msgid "" "Saving data changes was rolled back but the current transaction is still " "active; previous queries are unaffected." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1302 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1329 msgid "Data saved successfully." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1304 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1331 msgid "Auto-commit is off. You still need to commit changes to the database." msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1479 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1493 msgid "Geometry Viewer" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1548 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1648 #: pgadmin/tools/sqleditor/static/js/components/sections/ResultSetToolbar.jsx:439 #: pgadmin/utils/constants.py:32 msgid "Graph Visualiser" msgstr "" -#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1565 +#: pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx:1665 msgid "No data output. Execute a query to get output." msgstr "" diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx b/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx index c40256dba34..93da817b6dc 100644 --- a/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx +++ b/web/pgadmin/static/js/components/ReactCodeMirror/index.jsx @@ -19,6 +19,7 @@ import gettext from 'sources/gettext'; import { PgIconButton } from '../Buttons'; import { copyToClipboard } from '../../clipboard'; import { useDelayedCaller } from '../../custom_hooks'; +import epFormatSQL from '../../../../tools/ep/static/js/ExplainPostgreSQL/formatSQL'; import Editor from './components/Editor'; import CustomPropTypes from '../../custom_prop_types'; @@ -70,8 +71,9 @@ export default function CodeMirror({className, currEditor, showCopyBtn=false, cu const [showCopy, setShowCopy] = useState(false); const preferences = usePreferences().getPreferencesForModule('sqleditor'); const editorPrefs = usePreferences().getPreferencesForModule('editor'); + const epPrefs = usePreferences().getPreferencesForModule('ep'); - const formatSQL = (view)=>{ + const formatSQL = async (view)=>{ let selection = true, sql = view.getSelection(); /* New library does not support capitalize casing so if a user has set capitalize casing we will @@ -95,7 +97,17 @@ export default function CodeMirror({className, currEditor, showCopyBtn=false, cu sql = view.getValue(); selection = false; } - let formattedSql = format(sql,formatPrefs); + let formattedSql; + if (epPrefs.explain_postgresql_format) { + try { + formattedSql = await epFormatSQL(sql); + } catch (e) { + console.error('Error formatting SQL using Explain PostgreSQL API:', e); + formattedSql = format(sql,formatPrefs); + } + } else { + formattedSql = format(sql,formatPrefs); + } if(selection) { view.replaceSelection(formattedSql); } else { diff --git a/web/pgadmin/tools/__init__.py b/web/pgadmin/tools/__init__.py index 3e50a8dab4f..2989853387b 100644 --- a/web/pgadmin/tools/__init__.py +++ b/web/pgadmin/tools/__init__.py @@ -34,6 +34,9 @@ def register(self, app, options): from .debugger import blueprint as module app.register_blueprint(module) + from .ep import blueprint as module + app.register_blueprint(module) + from .erd import blueprint as module app.register_blueprint(module) diff --git a/web/pgadmin/tools/ep/__init__.py b/web/pgadmin/tools/ep/__init__.py new file mode 100644 index 00000000000..d96b0a3b6e4 --- /dev/null +++ b/web/pgadmin/tools/ep/__init__.py @@ -0,0 +1,185 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2026, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""A blueprint module implementing Explain PostgreSQL configuration.""" + +import json +import urllib.request +from flask import request +from flask_babel import gettext +from pgadmin.utils import PgAdminModule +from pgadmin.utils.preferences import Preferences +from pgadmin.utils.ajax import make_json_response +from pgadmin.user_login_check import pga_login_required + +MODULE_NAME = 'ep' + +class EPModule(PgAdminModule): + """Explain PostgreSQL configuration module for pgAdmin.""" + + LABEL = gettext('Explain PostgreSQL') + + def register_preferences(self): + """ + Register preferences for Explain PostgreSQL. + """ + + self.explain_postgresql_api = self.preference.register( + 'Explain PostgreSQL', 'explain_postgresql_api', + gettext("Explain PostgreSQL API"), 'text', 'https://explain.tensor.ru', + category_label=gettext('Configuration'), + help_str=gettext('Explain PostgreSQL API endpoint (e.g. https://explain-postgresql.com)'), + allow_blanks=False + ) + + self.explain_postgresql_private = self.preference.register( + 'Explain PostgreSQL', 'explain_postgresql_private', + gettext("Private Plans"), 'boolean', False, + category_label=gettext('Configuration'), + help_str=gettext('Hide plans from public access on Explain PostgreSQL') + ) + + self.explain_module = self.preference.register( + 'Explain PostgreSQL', 'explain_postgresql', + gettext("Explain Plan"), 'boolean', True, + category_label=gettext('Configuration'), + help_str=gettext('Analyze query plan via Explain PostgreSQL API') + ) + + self.explain_postgresql_format = self.preference.register( + 'Explain PostgreSQL', 'explain_postgresql_format', + gettext("Format SQL"), 'boolean', True, + category_label=gettext('Configuration'), + help_str=gettext('Format SQL using Explain PostgreSQL API') + ) + + def get_exposed_url_endpoints(self): + """ + Returns the list of URLs exposed to the client. + """ + return [ + 'ep.explain_postgresql', + 'ep.explain_postgresql_format', + ] + + +# Initialise the module +blueprint = EPModule(MODULE_NAME, __name__, static_url_path='/static') + +@blueprint.route( + '/explain_postgresql_format', + methods=["POST"], endpoint='explain_postgresql_format' +) +@pga_login_required +def explain_postgresql_format(): + """ + This method is used to send sql to explain postgresql beatifier api. + + """ + + data = request.get_json() + explain_postgresql_api = get_preference_value('explain_postgresql_api') + + is_error, data = send_post_request(explain_postgresql_api + '/beautifier-api', data) + if is_error: + return make_json_response(success=0, errormsg=data, + info=gettext('Failed to post data to the Explain Postgresql API'), + ) + + return make_json_response(success=1, data=data) + + +@blueprint.route( + '/explain_postgresql', + methods=["POST"], endpoint='explain_postgresql' +) +@pga_login_required +def explain_postgresql(): + """ + This method is used to send plan to explain postgresql api. + + """ + + data = request.get_json() + explain_postgresql_api = get_preference_value('explain_postgresql_api') + explain_postgresql_private = get_preference_value('explain_postgresql_private') + data['private'] = explain_postgresql_private + + is_error, data = send_post_request(explain_postgresql_api + '/explain', data) + if is_error: + return make_json_response(success=0, errormsg=data, + info=gettext('Failed to post data to the Explain Postgresql API'), + ) + + return make_json_response(success=1, data=explain_postgresql_api + data) + + +def send_post_request(url_api, data, parse=False): + data = json.dumps(data).encode('utf-8') + headers = { + "Content-Type": "application/json; charset=utf-8", + "User-Agent": "pgAdmin4/ExplainModule", + "Method": "POST" + } + try: + req = urllib.request.Request(url_api, data, headers) + with no302opener.open(req) as response: + if (response.code == 302): + return False, response.headers["Location"] + response_data = response.read().decode('utf-8') + if (parse): + return False, json.loads(response_data) + else: + return False, response_data + except Exception as e: + return True, str(e) + +class No302HTTPErrorProcessor(urllib.request.HTTPErrorProcessor): + + def http_response(self, request, response): + code, msg, hdrs = response.code, response.msg, response.info() + + if (code == 302): + return response + + # According to RFC 2616, "2xx" code indicates that the client's + # request was successfully received, understood, and accepted. + if not (200 <= code < 300): + response = self.parent.error( + 'http', request, response, code, msg, hdrs) + + return response + + https_response = http_response + +# opener = urllib.request.build_opener(No302HTTPErrorProcessor) +# urllib.request.install_opener(opener) +no302opener = urllib.request.build_opener(No302HTTPErrorProcessor) + +def get_preference_value(name): + """ + Get a preference value, returning None if empty or not set. + + Args: + name: The preference name (e.g., 'explain_postgresql_api') + + Returns: + The preference value or None if empty/not set. + """ + try: + pref_module = Preferences.module(MODULE_NAME) + if pref_module: + pref = pref_module.preference(name) + if pref: + value = pref.get() + if value and str(value).strip(): + return str(value).strip() + except Exception: + pass + return None diff --git a/web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/formatSQL.js b/web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/formatSQL.js new file mode 100644 index 00000000000..fff3a333de1 --- /dev/null +++ b/web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/formatSQL.js @@ -0,0 +1,32 @@ +import url_for from 'sources/url_for'; +import getApiInstance from '../../../../../static/js/api_instance'; + +export default async function formatSQL(sql) { + return new Promise((resolve, reject) => { + const api = getApiInstance(); + api.post( + url_for('ep.explain_postgresql_format'), + JSON.stringify({ + query_src: sql, + })) + .then((res) => { + if (res.data?.data) { + try { + const {btf_query, btf_query_text} = JSON.parse(res.data.data); + if (btf_query !== btf_query_text) { + resolve(btf_query_text); + } else { + reject(btf_query_text); + } + } catch (err) { + console.error(err); + reject(err.message); + } + } + }) + .catch((err) => { + console.error(err); + reject(err.message); + }); + }); +}; diff --git a/web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/index.jsx b/web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/index.jsx new file mode 100644 index 00000000000..e2d682b954a --- /dev/null +++ b/web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/index.jsx @@ -0,0 +1,91 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2026, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// +import { Box } from '@mui/material'; +import { styled } from '@mui/material/styles'; +import { useState, useEffect } from 'react'; +import _ from 'lodash'; +import gettext from 'sources/gettext'; +import url_for from 'sources/url_for'; +import getApiInstance from '../../../../../static/js/api_instance'; +import PropTypes from 'prop-types'; +import EmptyPanelMessage from '../../../../../static/js/components/EmptyPanelMessage'; + +const StyledBox = styled(Box)(({theme}) => ({ + '& .Explain-tabPanel': { + padding: '0 !important', + backgroundColor: theme.palette.background.default + ' !important', + } +})); + +export default function ExplainPostgreSQL({ + plans=[], + emptyMessage=gettext('Use the Explain/Explain Analyze button to generate the plan for a query. Alternatively, you can also execute "EXPLAIN (FORMAT JSON) [QUERY]".'), + sql='', +}) { + + const [data, setData] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + if (_.isEmpty(plans)) return; + const api = getApiInstance(); + api.post( + url_for('ep.explain_postgresql'), + JSON.stringify({ + plan: JSON.stringify(plans), + query: sql, + })) + .then((res) => { + if (res.data?.success) { + setData(res.data?.data); + } else { + setError(`${res.data?.info} : ${res.data?.errormsg}`); + } + }) + .catch((err) => { + setError(err?.message); + }) + .finally(() => { + setIsLoading(false); + }); + }, []); + + if(_.isEmpty(plans)) { + return ( + + {emptyMessage && } + + ); + } + if (isLoading) return

Loading...

; + if (error) return ( + + {} + + ); + return ( +
+