Skip to content

Add SQL formatting and query plan analysis module via explain-postgresql.com API#9726

Open
MGorkov wants to merge 6 commits intopgadmin-org:masterfrom
MGorkov:master
Open

Add SQL formatting and query plan analysis module via explain-postgresql.com API#9726
MGorkov wants to merge 6 commits intopgadmin-org:masterfrom
MGorkov:master

Conversation

@MGorkov
Copy link

@MGorkov MGorkov commented Mar 10, 2026

This PR introduces a new module that integrates with the Explain PostgreSQL API (https://explain-postgresql.com). It provides users with enhanced SQL query formatting and in-depth query plan analysis directly within pgAdmin4.

Key Features:

  • External Plan Analysis: Send query plans to the external API for a more detailed analysis.
  • SQL Formatting: Utilize the API's capabilities to automatically format and beautify SQL queries.

Summary by CodeRabbit

  • New Features

    • Explain PostgreSQL integration: new Explain panel for analyzing/displaying query plans from query results, option to send plans to Explain PostgreSQL API, and control over plan privacy.
    • Remote SQL formatting: option to format SQL via Explain PostgreSQL API with graceful fallback to local formatter and clear success/error feedback.
  • Localization

    • Updated translations across many languages to surface Explain PostgreSQL labels, messages, help text, and errors.

…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.
@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds an Explain PostgreSQL (EP) tool: a backend Flask blueprint with two POST endpoints for formatting and explaining SQL, frontend React components and API calls, async CodeMirror formatting that prefers EP (with local fallback), ResultSet integration emitting/opening EP plans, and translation updates.

Changes

Cohort / File(s) Summary
Explain PostgreSQL Backend
web/pgadmin/tools/ep/__init__.py, web/pgadmin/tools/__init__.py
New EPModule registered; adds preferences, explain_postgresql_format and explain_postgresql POST routes, URL validation, custom no-302 HTTP handling, and send_post_request helper.
Frontend EP UI & Formatting
web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/index.jsx, web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/formatSQL.js
New ExplainPostgreSQL React component rendering an iframe and managing loading/error states; formatSQL.js posts SQL to EP format endpoint and returns parsed formatted SQL with error handling.
CodeMirror Integration
web/pgadmin/static/js/components/ReactCodeMirror/index.jsx
Makes SQL formatting async, adds optional EP remote formatting via epFormatSQL when preference enabled, and falls back to local sql-formatter on failure.
Query Tool / ResultSet Integration
web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js, web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx
Adds QUERY_PLAN event and EXPLAIN_POSTGRESQL panel constant; ResultSet emits QUERY_PLAN and opens a closable Explain PostgreSQL tab that renders the EP component.
Localization / POT
web/pgadmin/messages.pot, web/pgadmin/translations/.../LC_MESSAGES/messages.po
Updated POT and many .po files to include EP-related msgids and header metadata updates across locales (new UI/API strings and translations).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (Browser)
    participant CM as CodeMirror
    participant API as pgAdmin API
    participant EP as Explain PostgreSQL API
    participant LF as LocalFormatter

    Client->>CM: Request SQL formatting
    CM->>CM: Check ep.explain_postgresql_format pref
    alt EP formatting enabled
        CM->>API: POST /explain_postgresql_format (SQL)
        API->>EP: Forward request
        EP-->>API: Formatted SQL or error
        alt success
            API-->>CM: Formatted SQL
        else failure
            API-->>CM: Error
            CM->>LF: Format locally
            LF-->>CM: Formatted SQL
        end
    else EP formatting disabled
        CM->>LF: Format locally
        LF-->>CM: Formatted SQL
    end
    CM-->>Client: Display formatted SQL
Loading
sequenceDiagram
    participant User as User
    participant Editor as ResultSet/Editor
    participant API as pgAdmin API
    participant EPComp as EP backend (blueprint)
    participant IFrame as Embedded IFrame

    User->>Editor: Execute query
    Editor->>Editor: Generate query plan JSON
    Editor->>API: POST /explain_postgresql (plan JSON + query)
    API->>EPComp: Forward request (using prefs)
    EPComp-->>API: Return plan URL / response
    API-->>Editor: Success with plan URL
    Editor->>Editor: Emit QUERY_PLAN event
    Editor->>Editor: Open ExplainPostgreSQL tab with plans and SQL
    ExplainPostgreSQL->>IFrame: Embed plan via returned URL
    IFrame-->>User: Render explain plan
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: introducing a new module that integrates pgAdmin with the Explain PostgreSQL API for SQL formatting and query plan analysis.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (25)
web/pgadmin/translations/id/LC_MESSAGES/messages.po-17902-17912 (1)

17902-17912: ⚠️ Potential issue | 🟡 Minor

Translate the new Geometry Viewer empty-state messages.

Both newly added status strings are still empty here, so Indonesian users will get English guidance in the empty/no-spatial-data paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/id/LC_MESSAGES/messages.po` around lines 17902 -
17912, The two new message entries in messages.po for the Geometry Viewer are
untranslated (empty msgstr) so Indonesian users see English; open the entries
for the msgid starting "Query complete. Use the Geometry Viewer button in the
Data Output tab to visualize results." and the msgid starting "No spatial data
found. At least one geometry or geography column is required for visualization."
and provide accurate Indonesian translations in their corresponding msgstr
fields (preserve punctuation and intent), then save the updated messages.po so
the GeometryViewer.jsx strings are localized.
web/pgadmin/translations/id/LC_MESSAGES/messages.po-15946-15981 (1)

15946-15981: ⚠️ Potential issue | 🟡 Minor

Populate the new Explain PostgreSQL entries.

Several new msgids in this block still have empty msgstrs. Even if brand labels stay in English, the new Explain PostgreSQL settings/help/error copy will otherwise fall back to English and ship as a mixed-language Indonesian UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/id/LC_MESSAGES/messages.po` around lines 15946 -
15981, The listed msgid entries ("Explain PostgreSQL", "Explain PostgreSQL API",
"Explain PostgreSQL API endpoint (e.g. https://explain-postgresql.com)",
"Private Plans", "Hide plans from public access on Explain PostgreSQL", "Explain
Plan", "Analyze query plan via Explain PostgreSQL API", "Format SQL using
Explain PostgreSQL API", and "Failed to post data to the Explain Postgresql
API") currently have empty msgstrs; populate each msgstr with the appropriate
Indonesian translation (or keep the brand name in English where desired) while
preserving punctuation and any examples, and ensure the duplicate/combined msgid
("Failed to post data to the Explain Postgresql API") is translated
consistently; update the translations in the messages.po entries corresponding
to those msgid strings so the UI shows Indonesian text instead of falling back
to English.
web/pgadmin/translations/zh_Hans_CN/LC_MESSAGES/messages.po-15617-15652 (1)

15617-15652: ⚠️ Potential issue | 🟡 Minor

Capitalization inconsistency in error message.

Line 15651 uses "Explain Postgresql API" (lowercase 's' in "Postgresql"), but all other occurrences use "PostgreSQL" with proper capitalization. This inconsistency will appear in error messages.

Proposed fix in the source file

The fix should be applied in the source file pgadmin/tools/ep/__init__.py at lines 92 and 117:

-msgid "Failed to post data to the Explain Postgresql API"
+msgid "Failed to post data to the Explain PostgreSQL API"

Missing Chinese translations for new EP-related strings.

All new Explain PostgreSQL feature strings have empty msgstr values. While this is acceptable for initial development, these should be translated for complete Simplified Chinese localization:

  • "Explain PostgreSQL"
  • "Explain PostgreSQL API"
  • "Explain PostgreSQL API endpoint (e.g. https://explain-postgresql.com)"
  • "Private Plans"
  • "Hide plans from public access on Explain PostgreSQL"
  • "Explain Plan"
  • "Analyze query plan via Explain PostgreSQL API"
  • "Format SQL using Explain PostgreSQL API"
  • "Failed to post data to the Explain Postgresql API"

Would you like me to open an issue to track the completion of these translations?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/zh_Hans_CN/LC_MESSAGES/messages.po` around lines
15617 - 15652, The messages.po entries for the new Explain PostgreSQL feature
are missing Chinese translations and one error string uses inconsistent
capitalization ("Explain Postgresql API"); update the source to consistently use
"Explain PostgreSQL" (fix the string in pgadmin/tools/ep/__init__.py where
"Failed to post data to the Explain Postgresql API" appears) and populate the
corresponding msgstr values in
web/pgadmin/translations/zh_Hans_CN/LC_MESSAGES/messages.po for the keys
"Explain PostgreSQL", "Explain PostgreSQL API", "Explain PostgreSQL API endpoint
(e.g. https://explain-postgresql.com)", "Private Plans", "Hide plans from public
access on Explain PostgreSQL", "Explain Plan", "Analyze query plan via Explain
PostgreSQL API", "Format SQL using Explain PostgreSQL API", and "Failed to post
data to the Explain PostgreSQL API".
web/pgadmin/translations/pt_BR/LC_MESSAGES/messages.po-17375-17385 (1)

17375-17385: ⚠️ Potential issue | 🟡 Minor

Missing translations for new GeometryViewer messages.

Two new user-facing strings lack Portuguese translations:

  • Line 17376-17378: "Query complete. Use the Geometry Viewer button in the Data Output tab to visualize results."
  • Line 17382-17384: "No spatial data found. At least one geometry or geography column is required for visualization."

These are informative messages that help users understand how to use the Geometry Viewer feature.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/pt_BR/LC_MESSAGES/messages.po` around lines 17375 -
17385, Add Portuguese translations for the two new GeometryViewer messages in
the messages.po file: provide a suitable pt_BR msgstr for the msgid "Query
complete. Use the Geometry Viewer button in the Data Output tab to visualize
results." and for the msgid "No spatial data found. At least one geometry or
geography column is required for visualization." so they replace the empty
msgstr entries; locate the entries associated with
pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx and
update each msgstr with the correct Portuguese phrasing.
web/pgadmin/translations/pt_BR/LC_MESSAGES/messages.po-17813-17951 (1)

17813-17951: ⚠️ Potential issue | 🟡 Minor

Several ResultSet strings have incomplete or truncated translations.

Some translations appear truncated or missing:

Line msgid msgstr (issue)
17818 "min" empty
17831 "Refetching latest results..." empty
17839 "Connection Error" empty
17844-17849 "Execution Cancelled!" / "Execution Cancelled" empty
17852 "Server Connected." empty
17904 "Fetching rows..." empty
17913 "The data has changed..." truncated to "Os dados foram alterados. "
17937 "Auto-commit is off..." truncated to "A confirmação automática está desativada. "
17951 "No data output..." truncated to "Sem saída de dados. "

The truncated translations (ending with a space and missing the rest of the sentence) should be completed to provide full context to users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/pt_BR/LC_MESSAGES/messages.po` around lines 17813 -
17951, Update the missing and truncated Portuguese translations in the
messages.po entries used by ResultSet (and related) components: fill in msgstr
for "min", "Refetching latest results...", "Connection Error", "Execution
Cancelled!", "Execution Cancelled", "Server Connected.", "Fetching rows...", and
complete the truncated sentences for "The data has changed. Do you want to save
changes?", "Auto-commit is off. You still need to commit changes to the
database." and "No data output. Execute a query to get output." Ensure each
msgstr is a full, grammatically correct Portuguese translation (preserve any
python-format markers like %, keep quotes and escaped characters as in the
original msgid) and remove the stray trailing spaces so strings are complete and
accurate for the ResultSet.jsx/ResultSetToolbar.jsx UI messages.
web/pgadmin/translations/pt_BR/LC_MESSAGES/messages.po-15486-15521 (1)

15486-15521: ⚠️ Potential issue | 🟡 Minor

Missing translations for new Explain PostgreSQL feature strings.

All new Explain PostgreSQL UI strings have empty msgstr values. While the application will fall back to English, Brazilian Portuguese users won't see localized text for this feature. Consider adding translations before release:

  • Line 15488: "Explain PostgreSQL"
  • Line 15492: "Explain PostgreSQL API"
  • Line 15496: "Explain PostgreSQL API endpoint..."
  • Line 15500: "Private Plans" → e.g., "Planos Privados"
  • Line 15504: "Hide plans from public access..." → e.g., "Ocultar planos do acesso público..."
  • Line 15508: "Explain Plan" → e.g., "Plano de Explicação"
  • Line 15512: "Analyze query plan via Explain PostgreSQL API"
  • Line 15516: "Format SQL using Explain PostgreSQL API"
  • Line 15520: "Failed to post data to the Explain Postgresql API"

Also note: Line 15520 has inconsistent capitalization "Postgresql" vs "PostgreSQL" used elsewhere in this file. The source string in pgadmin/tools/ep/__init__.py should use "PostgreSQL" for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/pt_BR/LC_MESSAGES/messages.po` around lines 15486 -
15521, The PO file contains empty msgstr entries for new Explain PostgreSQL UI
strings; fill the Portuguese translations for the msgids "Explain PostgreSQL",
"Explain PostgreSQL API", "Explain PostgreSQL API endpoint (e.g.
https://explain-postgresql.com)", "Private Plans", "Hide plans from public
access on Explain PostgreSQL", "Explain Plan", "Analyze query plan via Explain
PostgreSQL API", "Format SQL using Explain PostgreSQL API", and "Failed to post
data to the Explain Postgresql API" in
web/pgadmin/translations/pt_BR/LC_MESSAGES/messages.po so Brazilian Portuguese
users see localized text; also fix the inconsistent capitalization by correcting
the source string in pgadmin/tools/ep/__init__.py from "Postgresql" to
"PostgreSQL" (refer to the msgid "Failed to post data to the Explain Postgresql
API") so translations and source match.
web/pgadmin/translations/pl/LC_MESSAGES/messages.po-15608-15610 (1)

15608-15610: ⚠️ Potential issue | 🟡 Minor

Inconsistent capitalization in source string: "Postgresql" vs "PostgreSQL".

The error message uses "Explain Postgresql" (lowercase 'sql') while all other strings in this feature use "Explain PostgreSQL" (uppercase 'SQL'). This inconsistency originates from the source code in pgadmin/tools/ep/__init__.py at lines 92 and 117.

Suggested fix in source file (ep/__init__.py)
-msgid "Failed to post data to the Explain Postgresql API"
+msgid "Failed to post data to the Explain PostgreSQL API"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/pl/LC_MESSAGES/messages.po` around lines 15608 -
15610, Update the source string in pgadmin/tools/ep/__init__.py so the product
name is consistently capitalized as "Explain PostgreSQL" instead of "Explain
Postgresql"; locate the two occurrences referenced (the msgid used at the sites
around the code at the positions corresponding to lines 92 and 117) and change
the literal string to "Failed to post data to the Explain PostgreSQL API" so
translations and other messages remain consistent.
web/pgadmin/translations/zh_Hant_TW/LC_MESSAGES/messages.po-17498-17508 (1)

17498-17508: ⚠️ Potential issue | 🟡 Minor

Translate the new Geometry Viewer messages too.

Lines 17498-17508 introduce two new runtime messages with empty msgstrs, which leaves this path partially untranslated in zh_Hant_TW.

Sample zh_Hant_TW wording
 msgid ""
 "Query complete. Use the Geometry Viewer button in the Data Output tab to "
 "visualize results."
-msgstr ""
+msgstr ""
+"查詢完成。請使用「資料輸出」頁籤中的「幾何查看器」按鈕來視覺化結果。"

 msgid ""
 "No spatial data found. At least one geometry or geography column is "
 "required for visualization."
-msgstr ""
+msgstr ""
+"找不到空間資料。至少需要一個 geometry 或 geography 欄位才能進行視覺化。"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/zh_Hant_TW/LC_MESSAGES/messages.po` around lines
17498 - 17508, The two new runtime messages for the Geometry Viewer in the
translation file have empty msgstr entries—fill in the Traditional Chinese
(zh_Hant_TW) translations for the two msgid strings shown (the "Query complete.
Use the Geometry Viewer button in the Data Output tab to visualize results."
message and the "No spatial data found. At least one geometry or geography
column is required for visualization." message) in
web/pgadmin/translations/zh_Hant_TW/LC_MESSAGES/messages.po by replacing the
empty msgstrs with appropriate zh_Hant_TW translations so the GeometryViewer UI
strings are localized; ensure the translations match tone and context used by
GeometryViewer.jsx.
web/pgadmin/translations/zh_Hant_TW/LC_MESSAGES/messages.po-15613-15648 (1)

15613-15648: ⚠️ Potential issue | 🟡 Minor

Fill in the new Explain PostgreSQL translations.

Lines 15613-15648 add the full Explain PostgreSQL surface with empty msgstrs, so zh_Hant_TW will fall back to English for labels, help text, and the API error. Keeping the product name in English is fine, but these descriptive strings should not stay blank.

Sample zh_Hant_TW wording
 msgid "Explain PostgreSQL"
-msgstr ""
+msgstr "Explain PostgreSQL"

 msgid "Explain PostgreSQL API"
-msgstr ""
+msgstr "Explain PostgreSQL API"

 msgid "Explain PostgreSQL API endpoint (e.g. https://explain-postgresql.com)"
-msgstr ""
+msgstr "Explain PostgreSQL API 端點(例如 https://explain-postgresql.com)"

 msgid "Private Plans"
-msgstr ""
+msgstr "私有查詢計畫"

 msgid "Hide plans from public access on Explain PostgreSQL"
-msgstr ""
+msgstr "在 Explain PostgreSQL 上將查詢計畫設為非公開"

 msgid "Explain Plan"
-msgstr ""
+msgstr "查詢計畫分析"

 msgid "Analyze query plan via Explain PostgreSQL API"
-msgstr ""
+msgstr "透過 Explain PostgreSQL API 分析查詢計畫"

 msgid "Format SQL using Explain PostgreSQL API"
-msgstr ""
+msgstr "使用 Explain PostgreSQL API 格式化 SQL"

 msgid "Failed to post data to the Explain Postgresql API"
-msgstr ""
+msgstr "無法將資料傳送到 Explain PostgreSQL API"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/zh_Hant_TW/LC_MESSAGES/messages.po` around lines
15613 - 15648, Add Traditional Chinese (zh_Hant_TW) translations for the new
Explain PostgreSQL strings by filling each empty msgstr for the msgid entries
"Explain PostgreSQL", "Explain PostgreSQL API", "Explain PostgreSQL API endpoint
(e.g. https://explain-postgresql.com)", "Private Plans", "Hide plans from public
access on Explain PostgreSQL", "Explain Plan", "Analyze query plan via Explain
PostgreSQL API", "Format SQL using Explain PostgreSQL API", and "Failed to post
data to the Explain Postgresql API" with appropriate zh_Hant_TW text (you may
keep the product name "Explain PostgreSQL" in English where desired), ensuring
the translations convey the same UI/help/error meanings and preserve
placeholders/URLs exactly as in the msgid values.
web/pgadmin/translations/es/LC_MESSAGES/messages.po-15618-15653 (1)

15618-15653: ⚠️ Potential issue | 🟡 Minor

Missing Spanish translations for new Explain PostgreSQL feature strings.

All 9 new msgid entries for the Explain PostgreSQL feature have empty msgstr values. Spanish-speaking users will see English text for these UI elements:

  • "Explain PostgreSQL"
  • "Explain PostgreSQL API"
  • "Private Plans"
  • "Explain Plan"
  • API descriptions and error messages

Consider adding Spanish translations before release, or tracking this as a follow-up task to ensure localization completeness for the new feature.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/es/LC_MESSAGES/messages.po` around lines 15618 -
15653, The provided messages.po contains nine msgid entries with empty msgstr
values (e.g. "Explain PostgreSQL", "Explain PostgreSQL API", "Explain PostgreSQL
API endpoint (e.g. https://explain-postgresql.com)", "Private Plans", "Hide
plans from public access on Explain PostgreSQL", "Explain Plan", "Analyze query
plan via Explain PostgreSQL API", "Format SQL using Explain PostgreSQL API",
"Failed to post data to the Explain Postgresql API"); fill in appropriate
Spanish translations for each corresponding msgstr in the same file, ensure
translations preserve punctuation/URLs and grammatical context, and then
rebuild/compile the locale files so the new Spanish strings are shipped to
users.
web/pgadmin/translations/ko/LC_MESSAGES/messages.po-15708-15743 (1)

15708-15743: ⚠️ Potential issue | 🟡 Minor

Translate the new Explain PostgreSQL entries.

These new msgids are added with empty msgstr values, so the Korean catalog stays untranslated for the EP settings, actions, and failure message. Please populate these before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/ko/LC_MESSAGES/messages.po` around lines 15708 -
15743, The PO entries for Explain PostgreSQL are untranslated: fill the empty
msgstr values for the listed msgid strings ("Explain PostgreSQL", "Explain
PostgreSQL API", "Explain PostgreSQL API endpoint (e.g.
https://explain-postgresql.com)", "Private Plans", "Hide plans from public
access on Explain PostgreSQL", "Explain Plan", "Analyze query plan via Explain
PostgreSQL API", "Format SQL using Explain PostgreSQL API", "Failed to post data
to the Explain Postgresql API") (references: pgadmin/tools/ep/__init__.py and
ResultSet.jsx) by providing accurate Korean translations, preserving the msgid
text and PO formatting/escaping, encoding in UTF-8, and keeping the same quoting
and punctuation so the translations are valid in messages.po.
web/pgadmin/translations/ko/LC_MESSAGES/messages.po-17617-17627 (1)

17617-17627: ⚠️ Potential issue | 🟡 Minor

Localize the new Geometry Viewer notices.

Both new status messages are added with empty msgstr, which leaves the no-spatial-data path untranslated in the Korean UI. Please add Korean translations here as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/ko/LC_MESSAGES/messages.po` around lines 17617 -
17627, Add Korean translations for the two new msgid entries introduced for
Geometry Viewer notices so the UI shows localized strings instead of empty
msgstr values; locate the two msgid lines matching the exact text "Query
complete. Use the Geometry Viewer button in the Data Output tab to visualize
results." and "No spatial data found. At least one geometry or geography column
is required for visualization." in messages.po (entries referenced from
GeometryViewer.jsx) and fill each corresponding msgstr with appropriate Korean
translations that convey the same meaning.
web/pgadmin/translations/fr/LC_MESSAGES/messages.po-18088-18098 (1)

18088-18098: ⚠️ Potential issue | 🟡 Minor

Missing French translations for new GeometryViewer strings.

Two new GeometryViewer-related strings have empty translations:

  • "Query complete. Use the Geometry Viewer button in the Data Output tab to visualize results." (Lines 18089-18092)
  • "No spatial data found. At least one geometry or geography column is required for visualization." (Lines 18095-18098)

These provide user guidance for spatial data visualization and should be translated for French users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/fr/LC_MESSAGES/messages.po` around lines 18088 -
18098, The messages.po file contains two empty French msgstr entries for
GeometryViewer strings from GeometryViewer.jsx; add proper French translations
for the msgid "Query complete. Use the Geometry Viewer button in the Data Output
tab to visualize results." and for "No spatial data found. At least one geometry
or geography column is required for visualization." by filling their
corresponding msgstr values with accurate French text, ensuring punctuation and
placeholders (if any) match the original English, and save the updated
translations so the GeometryViewer component displays French text for these
messages.
web/pgadmin/translations/fr/LC_MESSAGES/messages.po-16091-16127 (1)

16091-16127: ⚠️ Potential issue | 🟡 Minor

Missing French translations for all Explain PostgreSQL strings.

All 9 new translation entries for the Explain PostgreSQL feature have empty msgstr values. French users will see English text instead of localized strings:

  • "Explain PostgreSQL" (Line 16094)
  • "Explain PostgreSQL API" (Line 16098)
  • "Explain PostgreSQL API endpoint..." (Line 16102)
  • "Private Plans" (Line 16106)
  • "Hide plans from public access..." (Line 16110)
  • "Explain Plan" (Line 16114)
  • "Analyze query plan via..." (Line 16118)
  • "Format SQL using..." (Line 16122)
  • "Failed to post data..." (Line 16126)

Consider adding French translations before merging, or track this as a follow-up localization task.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/fr/LC_MESSAGES/messages.po` around lines 16091 -
16127, Fill the empty msgstr entries for the nine Explain PostgreSQL msgid
strings by providing proper French translations: "Explain PostgreSQL", "Explain
PostgreSQL API", "Explain PostgreSQL API endpoint (e.g.
https://explain-postgresql.com)", "Private Plans", "Hide plans from public
access on Explain PostgreSQL", "Explain Plan", "Analyze query plan via Explain
PostgreSQL API", "Format SQL using Explain PostgreSQL API", and "Failed to post
data to the Explain Postgresql API"; update each corresponding msgstr with an
accurate French equivalent (preserving casing and contextual meaning) so the UI
shows localized text for the Explain PostgreSQL feature.
web/pgadmin/translations/de/LC_MESSAGES/messages.po-15578-15584 (1)

15578-15584: ⚠️ Potential issue | 🟡 Minor

Normalize the EP source copy before regenerating this catalog.

Line 15583 uses the fairly technical API endpoint wording for a preferences field, and Line 15607 switches to Explain Postgresql while the rest of the feature uses Explain PostgreSQL. Please simplify the source text in pgadmin/tools/ep/__init__.py and keep the product name spelling consistent before extracting translations. Based on learnings, help messages for form controls should be kept concise and avoid technical jargon or terminology that might sound like error states; simple descriptions are preferred.

Also applies to: 15606-15608

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/de/LC_MESSAGES/messages.po` around lines 15578 -
15584, Update the source strings in pgadmin/tools/ep/__init__.py so the
preference help text is concise and the product name spelling is consistent:
replace the msgid "Explain PostgreSQL API" with a simpler label such as "Explain
PostgreSQL" and change "Explain PostgreSQL API endpoint (e.g.
https://explain-postgresql.com)" to a short, non-technical help text like
"Explain PostgreSQL (e.g. https://explain-postgresql.com)" to avoid the term
"API endpoint" and ensure "PostgreSQL" is capitalized consistently across the
strings referenced above.
web/pgadmin/translations/de/LC_MESSAGES/messages.po-15573-15608 (1)

15573-15608: ⚠️ Potential issue | 🟡 Minor

Please fill in the new Explain PostgreSQL entries for de.

All of the new EP-specific entries in this block still have empty msgstrs. That ships the new settings/actions/error text as raw English in the German locale.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/de/LC_MESSAGES/messages.po` around lines 15573 -
15608, The new Explain PostgreSQL translation entries in
pgadmin/tools/ep/__init__.py (the msgid values "Explain PostgreSQL", "Explain
PostgreSQL API", "Explain PostgreSQL API endpoint (e.g.
https://explain-postgresql.com)", "Private Plans", "Hide plans from public
access on Explain PostgreSQL", "Explain Plan", "Analyze query plan via Explain
PostgreSQL API", "Format SQL using Explain PostgreSQL API", and "Failed to post
data to the Explain Postgresql API") currently have empty msgstrs in the German
.po file; fill each corresponding msgstr with an appropriate German translation
that preserves meaning and tone for the UI/error messages (ensure punctuation
and capitalization match original intent).
web/pgadmin/translations/ru/LC_MESSAGES/messages.po-18207-18215 (1)

18207-18215: ⚠️ Potential issue | 🟡 Minor

Translate the new Save Data messages.

These two msgstr entries are empty, so the Russian flow falls back to English exactly where it explains generated SQL and rollback state. Suggested translations: Этот запрос был сгенерирован pgAdmin в рамках операции «Сохранить данные». and Сохранение изменений данных было отменено откатом, но текущая транзакция всё ещё активна; предыдущие запросы не затронуты.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/ru/LC_MESSAGES/messages.po` around lines 18207 -
18215, Add Russian translations for the two empty msgstr entries corresponding
to the msgid strings in ResultSet.jsx: set msgstr for "This query was generated
by pgAdmin as part of a \"Save Data\" operation" to "Этот запрос был
сгенерирован pgAdmin в рамках операции «Сохранить данные»." and set msgstr for
"Saving data changes was rolled back but the current transaction is still
active; previous queries are unaffected." to "Сохранение изменений данных было
отменено откатом, но текущая транзакция всё ещё активна; предыдущие запросы не
затронуты."
web/pgadmin/translations/ru/LC_MESSAGES/messages.po-17614-17629 (1)

17614-17629: ⚠️ Potential issue | 🟡 Minor

Fix the plural grammar in these Geometry Viewer warnings.

3D geometries not rendered. is translated as singular, and Geometries with non-SRID %s not rendered. mixes singular/plural and reads broken as не-SRID %s. Please keep both messages plural and preserve %s, e.g. 3D-геометрии не отображаются. / Геометрии с SRID, отличным от %s, не отображаются.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/ru/LC_MESSAGES/messages.po` around lines 17614 -
17629, Update the Russian translations in messages.po for the GeometryViewer
strings to use correct plural forms and preserve the %s placeholder: change the
translation for msgid "3D geometries not rendered." to a plural form (e.g.
"3D-геометрии не отображаются."), fix "Geometries with non-SRID %s not
rendered." to a plural, correctly formatted sentence preserving %s (e.g.
"Геометрии с SRID, отличным от %s, не отображаются."), and ensure the msgid
mappings in the GeometryViewer.jsx references remain unchanged so the keys still
match.
web/pgadmin/translations/ru/LC_MESSAGES/messages.po-17655-17665 (1)

17655-17665: ⚠️ Potential issue | 🟡 Minor

Add the missing Russian translations for the new Geometry Viewer states.

Both msgstr entries are empty, so RU users will see English for the success and empty-state messages. Suggested translations: Запрос завершён. Используйте кнопку «Просмотрщик геометрии» на вкладке «Вывод данных», чтобы визуализировать результаты. and Пространственные данные не найдены. Для визуализации требуется как минимум один столбец geometry или geography.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/ru/LC_MESSAGES/messages.po` around lines 17655 -
17665, Fill the two empty msgstr entries in messages.po that correspond to the
Geometry Viewer messages used in GeometryViewer.jsx: set the msgstr for "Query
complete. Use the Geometry Viewer button in the Data Output tab to visualize
results." to "Запрос завершён. Используйте кнопку «Просмотрщик геометрии» на
вкладке «Вывод данных», чтобы визуализировать результаты." and set the msgstr
for "No spatial data found. At least one geometry or geography column is
required for visualization." to "Пространственные данные не найдены. Для
визуализации требуется как минимум один столбец geometry или geography."
web/pgadmin/translations/ru/LC_MESSAGES/messages.po-14561-14565 (1)

14561-14565: ⚠️ Potential issue | 🟡 Minor

Translate Analysis before reusing it in the tree view.

Line 14564 adds a new PgTreeView reference, but msgstr is still empty. The Russian UI will fall back to English on that path. Suggested msgstr: Анализ.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/ru/LC_MESSAGES/messages.po` around lines 14561 -
14565, The new msgid "Analysis" in messages.po is untranslated so the Russian UI
will fall back to English; open the translation entry for msgid "Analysis" in
web/pgadmin/translations/ru/LC_MESSAGES/messages.po and set msgstr to the
Russian translation (suggested: "Анализ"), ensuring the same msgid is used by
PgTreeView (PgTreeView/index.jsx) so the tree view displays the localized
string.
web/pgadmin/translations/ru/LC_MESSAGES/messages.po-15740-15747 (1)

15740-15747: ⚠️ Potential issue | 🟡 Minor

Keep the Explain PostgreSQL labels aligned with the source meaning.

On Line 15742 the service name becomes a generic Анализ плана выполнения, on Line 15746 the group name turns into Адрес API..., and on Line 15764 the singular Explain Plan becomes plural. That blurs the new external Explain PostgreSQL integration with pgAdmin's built-in Explain flow. Please keep the service/group names literal and the plan label singular.

Also applies to: 15763-15765

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/ru/LC_MESSAGES/messages.po` around lines 15740 -
15747, The translations for the service/group/label are too liberal—ensure the
msgstr values match the source literally: update the msgstr for "Explain
PostgreSQL" to preserve the exact service name (do not translate to a generic
"Анализ плана выполнения"), change "Explain PostgreSQL API" msgstr to the
literal group name (not "Адрес API..."), and make the "Explain Plan" translation
singular (not plural); apply the same corrections to the other occurrence range
noted (15763-15765).
web/pgadmin/translations/ja/LC_MESSAGES/messages.po-17619-17630 (1)

17619-17630: ⚠️ Potential issue | 🟡 Minor

Missing Japanese translations for new GeometryViewer guidance messages.

Two new user-facing messages for the Geometry Viewer have empty msgstr values:

  • "Query complete. Use the Geometry Viewer button in the Data Output tab to visualize results."
  • "No spatial data found. At least one geometry or geography column is required for visualization."

These guidance messages help users understand how to use the Geometry Viewer feature and should be translated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/ja/LC_MESSAGES/messages.po` around lines 17619 -
17630, Add Japanese translations for the two empty msgstr entries in messages.po
corresponding to the Geometry Viewer guidance messages found in
GeometryViewer.jsx; locate the two msgid strings ("Query complete. Use the
Geometry Viewer button in the Data Output tab to visualize results." and "No
spatial data found. At least one geometry or geography column is required for
visualization.") and provide accurate Japanese translations in their msgstr
fields so the UI displays localized guidance for the GeometryViewer component.
web/pgadmin/translations/ja/LC_MESSAGES/messages.po-15745-15747 (1)

15745-15747: ⚠️ Potential issue | 🟡 Minor

Fix inconsistent capitalization: "Postgresql" should be "PostgreSQL".

Lines 92 and 117 in pgadmin/tools/ep/__init__.py use "Explain Postgresql API" with lowercase "sql", while all other references in the file consistently use "Explain PostgreSQL" (uppercase "SQL"). Standardize to "PostgreSQL" for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/ja/LC_MESSAGES/messages.po` around lines 15745 -
15747, Update the inconsistent capitalization of "Postgresql" to "PostgreSQL" by
changing the msgid "Failed to post data to the Explain Postgresql API" to
"Failed to post data to the Explain PostgreSQL API" in the translation entry and
also update the matching strings in the ep package's __init__.py where that
message is emitted (replace "Explain Postgresql API" => "Explain PostgreSQL API"
in both occurrences so source code and .po stay consistent).
web/pgadmin/translations/ja/LC_MESSAGES/messages.po-15712-15747 (1)

15712-15747: ⚠️ Potential issue | 🟡 Minor

Missing Japanese translations for new Explain PostgreSQL feature strings.

All new entries related to the Explain PostgreSQL feature have empty msgstr values. These should be translated before release to provide a complete localized experience for Japanese users.

Key strings requiring translation:

  • "Explain PostgreSQL"
  • "Explain PostgreSQL API"
  • "Private Plans"
  • "Explain Plan"
  • API endpoint descriptions and error messages
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/ja/LC_MESSAGES/messages.po` around lines 15712 -
15747, The PO file contains empty msgstr translations for the new Explain
PostgreSQL strings (msgid values like "Explain PostgreSQL", "Explain PostgreSQL
API", "Explain PostgreSQL API endpoint (e.g. https://explain-postgresql.com)",
"Private Plans", "Hide plans from public access on Explain PostgreSQL", "Explain
Plan", "Analyze query plan via Explain PostgreSQL API", "Format SQL using
Explain PostgreSQL API", and "Failed to post data to the Explain Postgresql
API"); update the Japanese translations by providing appropriate Japanese text
for each msgid in web/pgadmin/translations/ja/LC_MESSAGES/messages.po so every
msgstr is filled, ensuring wording matches existing localization style and
preserves placeholders and capitalization exactly as in the msgid entries.
web/pgadmin/messages.pot-15474-15476 (1)

15474-15476: ⚠️ Potential issue | 🟡 Minor

Inconsistent casing: "Postgresql" should be "PostgreSQL".

The error message uses "Postgresql" (lowercase 'ql') while all other EP-related strings consistently use "PostgreSQL" (uppercase SQL) to match the official branding.

Proposed fix

The fix needs to be applied in the source file pgadmin/tools/ep/__init__.py at lines 92 and 117:

-"Failed to post data to the Explain Postgresql API"
+"Failed to post data to the Explain PostgreSQL API"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/messages.pot` around lines 15474 - 15476, Update the translation
string "Failed to post data to the Explain Postgresql API" to use the correct
branding "Failed to post data to the Explain PostgreSQL API" in both occurrences
of that msgid found in the Explain Plan module (the two msgid entries
corresponding to the Explain Postgresql API); ensure both instances are changed
exactly and regenerate/update the .pot/.po files as needed so casing is
consistent across the EP-related messages.
🧹 Nitpick comments (7)
web/pgadmin/translations/zh_Hans_CN/LC_MESSAGES/messages.po (1)

17454-17514: Missing Chinese translations for GeometryViewer messages.

Two new GeometryViewer messages at lines 17503-17513 have empty msgstr values:

  • "Query complete. Use the Geometry Viewer button in the Data Output tab to visualize results."
  • "No spatial data found. At least one geometry or geography column is required for visualization."

These should be translated for complete localization coverage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/zh_Hans_CN/LC_MESSAGES/messages.po` around lines
17454 - 17514, Add Chinese translations for the two empty msgstr entries
corresponding to the GeometryViewer messages: translate "Query complete. Use the
Geometry Viewer button in the Data Output tab to visualize results." and "No
spatial data found. At least one geometry or geography column is required for
visualization." by filling their msgstr values in the messages.po so they match
the style of other entries referenced from GeometryViewer.jsx (use clear,
idiomatic Simplified Chinese).
web/pgadmin/translations/pt_BR/LC_MESSAGES/messages.po (1)

11024-11027: Additional empty translations for general UI strings.

Several non-EP related strings also lack translations:

  • Line 11026: "Configuration" → "Configuração"
  • Line 13511: "No files/folders found" → "Nenhum arquivo/pasta encontrado"
  • Line 14330: "No objects are found to display" → "Nenhum objeto encontrado para exibir"

These are common UI messages that would benefit from localization.

Also applies to: 13510-13512, 14329-14331

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/pt_BR/LC_MESSAGES/messages.po` around lines 11024 -
11027, Add Portuguese translations for the missing UI strings in the messages.po
file by filling the msgstr for each msgid noted in the review: set msgid
"Configuration" → msgstr "Configuração", msgid "No files/folders found" → msgstr
"Nenhum arquivo/pasta encontrado", and msgid "No objects are found to display" →
msgstr "Nenhum objeto encontrado para exibir"; ensure you update the existing
entries for those exact msgid values (e.g., the entries shown around the current
diff for pgadmin/tools/ep) and keep the PO file formatting and encoding intact.
web/pgadmin/translations/pl/LC_MESSAGES/messages.po (1)

15575-15610: New Explain PostgreSQL strings await Polish translation.

All new strings for the Explain PostgreSQL feature have empty msgstr values. While this is expected when adding new source strings (translations are typically done in a separate pass), please ensure these are tracked for translation before release to Polish users.

New untranslated strings:

  • "Explain PostgreSQL"
  • "Explain PostgreSQL API"
  • "Explain PostgreSQL API endpoint..."
  • "Private Plans"
  • "Hide plans from public access..."
  • "Explain Plan"
  • "Analyze query plan via..."
  • "Format SQL using..."
  • "Failed to post data..."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/translations/pl/LC_MESSAGES/messages.po` around lines 15575 -
15610, The new Explain PostgreSQL UI strings in the PO file (msgids such as
"Explain PostgreSQL", "Explain PostgreSQL API", "Explain PostgreSQL API endpoint
(e.g. https://explain-postgresql.com)", "Private Plans", "Hide plans from public
access on Explain PostgreSQL", "Explain Plan", "Analyze query plan via Explain
PostgreSQL API", "Format SQL using Explain PostgreSQL API" and "Failed to post
data to the Explain Postgresql API") currently have empty msgstr entries; update
messages.po by providing Polish translations for each msgid (located under
references pgadmin/tools/ep/__init__.py and pgadmin/tools/sqleditor/...) so the
UI is localized for Polish users, then run the usual PO validation/compilation
(msgfmt or project i18n tool) to ensure no syntax errors.
web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/index.jsx (2)

67-67: Inconsistent loading state styling.

The loading state renders a plain <p>Loading...</p> while empty and error states use StyledBox with EmptyPanelMessage. Consider using consistent styling or the project's Loader component for a unified UX.

♻️ Proposed fix for consistent styling
-  if (isLoading) return <p>Loading...</p>;
+  if (isLoading) return (
+    <StyledBox height="100%" display="flex" flexDirection="column">
+      <EmptyPanelMessage text={gettext('Loading...')} />
+    </StyledBox>
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/index.jsx` at line 67, The
loading branch currently returns a plain <p> via the isLoading check, causing
inconsistent styling; update the isLoading rendering in the ExplainPostgreSQL
component to use the same UI pattern as empty/error states (e.g., wrap the
Loader component inside StyledBox and/or display EmptyPanelMessage style) so
loading uses the project Loader and StyledBox instead of a raw paragraph; locate
the isLoading conditional and replace the simple <p>Loading...</p> with a Loader
(or the existing EmptyPanelMessage markup inside StyledBox) to match the other
states.

36-58: Missing dependencies in useEffect may cause stale data.

The useEffect has an empty dependency array [], but it uses plans and sql props. If this component instance receives new props (e.g., from a parent re-render), the effect won't re-fetch. While ResultSet.jsx currently creates new tab instances per plan, adding dependencies would make this component more robust for future reuse.

♻️ Proposed fix to add dependencies
   useEffect(() => {
     if (_.isEmpty(plans)) return;
     const api = getApiInstance();
+    setIsLoading(true);
+    setError('');
     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);
       });
-  }, []);
+  }, [plans, sql]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/index.jsx` around lines 36 -
58, The useEffect in ExplainPostgreSQL uses plans and sql but has an empty
dependency array, leading to stale fetches; update the effect's dependency array
to include plans and sql (and any other external values used inside like
getApiInstance if it's not stable) so the POST is re-run when those props
change, keeping the logic in the effect (the API.post call, setData, setError,
setIsLoading) the same and ensuring you still guard with _.isEmpty(plans) at the
top of the effect.
web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx (1)

1025-1038: Missing cleanup function for event listener registration.

The useEffect registers a listener for QUERY_TOOL_EVENTS.QUERY_PLAN but doesn't return a cleanup function. eventBus.registerListener returns a deregister function (as seen in lines 1118-1123 with deregExec). Without cleanup, re-mounting this component could register duplicate listeners.

♻️ Proposed fix to add cleanup
 useEffect(() => {
-   eventBus.registerListener(QUERY_TOOL_EVENTS.QUERY_PLAN, (planJson, sql) => {
+   const deregQueryPlan = eventBus.registerListener(QUERY_TOOL_EVENTS.QUERY_PLAN, (planJson, sql) => {
     if (!planJson) return;
     layoutDocker.openTab({
       id: PANELS.EXPLAIN_POSTGRESQL,
       title: gettext('Explain PostgreSQL'),
       content: <ExplainPostgreSQL
         plans={planJson}
         sql={sql}
       />,
       closable: true,
     }, PANELS.MESSAGES, 'after-tab', true);
   });
+   return () => {
+     deregQueryPlan();
+   };
 }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx`
around lines 1025 - 1038, The effect registers a listener via
eventBus.registerListener for QUERY_TOOL_EVENTS.QUERY_PLAN inside the useEffect
in ResultSet.jsx but never deregisters it; capture the deregister function
returned by eventBus.registerListener (e.g., const deregPlan =
eventBus.registerListener(...)) and return a cleanup function from that
useEffect that calls the deregister (e.g., return () => deregPlan()); keep the
same empty dependency array and ensure the listener callback signature
(planJson, sql) remains unchanged.
web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/formatSQL.js (1)

4-31: Consider simplifying with async/await instead of explicit Promise constructor.

The function is already marked async but wraps an explicit new Promise(). This is the Promise constructor anti-pattern. Using async/await directly would be cleaner and avoid the hanging Promise issue.

♻️ Proposed refactor using async/await
-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);
-      });
-  });
-};
+export default async function formatSQL(sql) {
+  const api = getApiInstance();
+  const res = await api.post(
+    url_for('ep.explain_postgresql_format'),
+    JSON.stringify({
+      query_src: sql,
+    })
+  );
+  
+  if (!res.data?.data) {
+    throw new Error('No data returned from formatting API');
+  }
+  
+  const { btf_query, btf_query_text } = JSON.parse(res.data.data);
+  if (btf_query !== btf_query_text) {
+    return btf_query_text;
+  }
+  // No formatting changes needed - return original
+  return sql;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/formatSQL.js` around lines 4
- 31, The formatSQL function wraps an explicit new Promise inside an async
function; remove the Promise constructor and convert the control flow to
async/await: await the api.post call (using getApiInstance() and
url_for('ep.explain_postgresql_format')), then inside a try/catch parse
res.data?.data with JSON.parse to extract btf_query and btf_query_text and
return btf_query_text when btf_query !== btf_query_text or throw an
Error(btf_query_text) when they are equal; also throw meaningful errors when
res.data?.data is missing or JSON.parse fails and surface caught error.message
in the thrown Error so callers receive the same failure information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4c016482-d146-407e-bfd5-415b710ca256

📥 Commits

Reviewing files that changed from the base of the PR and between a0e6da0 and 2c0e741.

📒 Files selected for processing (34)
  • web/pgadmin/messages.pot
  • web/pgadmin/static/js/components/ReactCodeMirror/index.jsx
  • web/pgadmin/tools/__init__.py
  • web/pgadmin/tools/ep/__init__.py
  • web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/formatSQL.js
  • web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/index.jsx
  • web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js
  • web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx
  • web/pgadmin/translations/cs/LC_MESSAGES/messages.mo
  • web/pgadmin/translations/cs/LC_MESSAGES/messages.po
  • web/pgadmin/translations/de/LC_MESSAGES/messages.mo
  • web/pgadmin/translations/de/LC_MESSAGES/messages.po
  • web/pgadmin/translations/es/LC_MESSAGES/messages.mo
  • web/pgadmin/translations/es/LC_MESSAGES/messages.po
  • web/pgadmin/translations/fr/LC_MESSAGES/messages.mo
  • web/pgadmin/translations/fr/LC_MESSAGES/messages.po
  • web/pgadmin/translations/id/LC_MESSAGES/messages.mo
  • web/pgadmin/translations/id/LC_MESSAGES/messages.po
  • web/pgadmin/translations/it/LC_MESSAGES/messages.mo
  • web/pgadmin/translations/it/LC_MESSAGES/messages.po
  • web/pgadmin/translations/ja/LC_MESSAGES/messages.mo
  • web/pgadmin/translations/ja/LC_MESSAGES/messages.po
  • web/pgadmin/translations/ko/LC_MESSAGES/messages.po
  • web/pgadmin/translations/pl/LC_MESSAGES/messages.mo
  • web/pgadmin/translations/pl/LC_MESSAGES/messages.po
  • web/pgadmin/translations/pt_BR/LC_MESSAGES/messages.mo
  • web/pgadmin/translations/pt_BR/LC_MESSAGES/messages.po
  • web/pgadmin/translations/ru/LC_MESSAGES/messages.mo
  • web/pgadmin/translations/ru/LC_MESSAGES/messages.po
  • web/pgadmin/translations/sv/LC_MESSAGES/messages.po
  • web/pgadmin/translations/zh_Hans_CN/LC_MESSAGES/messages.mo
  • web/pgadmin/translations/zh_Hans_CN/LC_MESSAGES/messages.po
  • web/pgadmin/translations/zh_Hant_TW/LC_MESSAGES/messages.mo
  • web/pgadmin/translations/zh_Hant_TW/LC_MESSAGES/messages.po

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
web/pgadmin/tools/ep/__init__.py (3)

34-40: Minor: Default URL differs from help text example.

The default value 'https://explain.tensor.ru' differs from the example in help_str (https://explain-postgresql.com). This could confuse users about which endpoint is canonical.

Consider aligning them for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/__init__.py` around lines 34 - 40, The default value for
the explain_postgresql_api preference is inconsistent with the help_str example;
update the call to preference.register that sets explain_postgresql_api so the
default URL matches the example in help_str (i.e., change the default from
'https://explain.tensor.ru' to 'https://explain-postgresql.com') by editing the
explain_postgresql_api registration in the __init__.py where preference.register
is invoked.

185-203: Inconsistent indentation: uses 2 spaces instead of 4.

This function uses 2-space indentation while the rest of the file uses 4 spaces. This inconsistency affects readability and violates PEP 8 conventions.

♻️ Fix indentation to 4 spaces
 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, timeout=10) 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)
+    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, timeout=10) 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)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/__init__.py` around lines 185 - 203, The
send_post_request function is indented with 2 spaces instead of the project
standard 4; reformat the entire function block (including the body, try/except
and all inner statements like the Request creation, with no302opener.open,
response handling and return statements) to use 4-space indentation so it
matches the rest of the file and PEP8 conventions.

250-252: Silent exception handling may obscure preference configuration issues.

The try-except-pass pattern silently ignores all exceptions, which could hide misconfigured preferences or module registration issues. Consider logging the exception for debugging purposes.

🔧 Add logging for debugging
+import logging
+
+logger = logging.getLogger(__name__)
+
 def get_preference_value(name):
     ...
     try:
         pref_module = Preferences.module(MODULE_NAME)
         ...
-    except Exception:
-        pass
+    except Exception as e:
+        logger.debug("Failed to get preference '%s': %s", name, e)
     return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/__init__.py` around lines 250 - 252, The except
Exception: pass silently swallows errors in the try block in
web/pgadmin/tools/ep/__init__.py; replace the pass with a logged exception so
failures are visible. Add a module logger (logger = logging.getLogger(__name__))
or use current_app.logger if in Flask context, then call
logger.exception("Failed loading preference/module in <function-or-block-name>")
(or logger.error with exc_info=True) inside the except. Ensure you import
logging (or current_app) at the top so the exception and stack trace are
recorded instead of being ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/pgadmin/tools/ep/__init__.py`:
- Around line 205-221: The No302HTTPErrorProcessor class is indented with tab
characters; replace all leading tabs with 4-space indentation in the class
definition and its methods (http_response and https_response) so indentation
matches the rest of the file and avoids TabError/IndentationError; ensure the
method bodies and the "https_response = http_response" assignment use spaces,
run a linter/autoformat (e.g., black or flake8) to confirm consistent spacing,
and commit the updated indentation only.
- Around line 146-152: The code assumes send_post_request returns a redirect
path and blindly concatenates explain_postgresql_api + data; change the response
handling in the block using send_post_request so it explicitly handles redirect
vs non-redirect: inspect the returned data (or status indicator from
send_post_request) and if data is a relative path (e.g., startswith('/') or is a
full URL) build the final URL using explain_postgresql_api + data (or use data
directly if absolute), otherwise treat data as a non-redirect body (attempt to
parse JSON or return it as-is) and return make_json_response(success=1,
data=parsed_body) instead of concatenating; reference send_post_request,
explain_postgresql_api and make_json_response to find and update the logic.
- Around line 175-180: The SSRF check currently only ensures parsed.hostname is
present; update the validation to resolve parsed.hostname to IP addresses and
use the ipaddress module to reject any loopback, localhost, link-local
(169.254.0.0/16), private (RFC1918), unspecified, multicast or other non-global
addresses before returning True; specifically, after obtaining parsed.hostname
(the variable named parsed.hostname in this file), perform DNS resolution for
both A and AAAA records, iterate over each resolved IP and parse it with
ipaddress.ip_address(), and return False if any address .is_loopback,
.is_private, .is_link_local, .is_unspecified, .is_multicast (or other non-global
checks) is True, otherwise allow; if hostname resolution fails or raises an
exception, treat it as invalid and return False; alternatively enforce that only
administrators can set explain_postgresql_api if you prefer a policy change
instead of IP blocking.

---

Nitpick comments:
In `@web/pgadmin/tools/ep/__init__.py`:
- Around line 34-40: The default value for the explain_postgresql_api preference
is inconsistent with the help_str example; update the call to
preference.register that sets explain_postgresql_api so the default URL matches
the example in help_str (i.e., change the default from
'https://explain.tensor.ru' to 'https://explain-postgresql.com') by editing the
explain_postgresql_api registration in the __init__.py where preference.register
is invoked.
- Around line 185-203: The send_post_request function is indented with 2 spaces
instead of the project standard 4; reformat the entire function block (including
the body, try/except and all inner statements like the Request creation, with
no302opener.open, response handling and return statements) to use 4-space
indentation so it matches the rest of the file and PEP8 conventions.
- Around line 250-252: The except Exception: pass silently swallows errors in
the try block in web/pgadmin/tools/ep/__init__.py; replace the pass with a
logged exception so failures are visible. Add a module logger (logger =
logging.getLogger(__name__)) or use current_app.logger if in Flask context, then
call logger.exception("Failed loading preference/module in
<function-or-block-name>") (or logger.error with exc_info=True) inside the
except. Ensure you import logging (or current_app) at the top so the exception
and stack trace are recorded instead of being ignored.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 62662a27-c81d-46d6-ace6-9e4f335eb064

📥 Commits

Reviewing files that changed from the base of the PR and between 2c0e741 and 28fe939.

📒 Files selected for processing (2)
  • web/pgadmin/tools/ep/__init__.py
  • web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/formatSQL.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/formatSQL.js

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
web/pgadmin/tools/ep/__init__.py (3)

193-195: ⚠️ Potential issue | 🟠 Major

no302opener still follows redirects before your custom processor sees them.

Passing HTTPHandler() and HTTPSHandler() into build_opener() does not remove the default HTTPRedirectHandler. The 302 branch in send_post_request() is therefore still bypassed, so this helper reads the redirected response instead of the Location header it expects.

#!/bin/bash
python - <<'PY'
import urllib.request

class No302HTTPErrorProcessor(urllib.request.HTTPErrorProcessor):
    pass

opener = urllib.request.build_opener(
    No302HTTPErrorProcessor(),
    urllib.request.HTTPHandler(),
    urllib.request.HTTPSHandler(),
)

for idx, handler in enumerate(opener.handlers):
    print(f"{idx}: {handler.__class__.__name__}")
PY

Also applies to: 204-227

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/__init__.py` around lines 193 - 195, The opener still
follows redirects because HTTPRedirectHandler is present by default; update the
code that builds no302opener (used by send_post_request) to include a custom
HTTPRedirectHandler that disables redirects (e.g., subclass
urllib.request.HTTPRedirectHandler and override redirect_request to return None)
so the 302 response and Location header are preserved, then use that custom
handler in build_opener in place of the current No302HTTPErrorProcessor-only
opener; apply the same change to the other opener creation mentioned around the
204-227 block so both places stop following redirects before your processor sees
them.

146-152: ⚠️ Potential issue | 🟠 Major

Make the explain success payload explicit instead of concatenating arbitrary helper output.

send_post_request() can return either a redirect target or the raw response body, but this route always does explain_postgresql_api + data. That only works for relative redirect paths; a 200 body or an absolute URL produces malformed output for the client.

Also applies to: 184-200

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/__init__.py` around lines 146 - 152, The handler
currently returns make_json_response(success=1, data=explain_postgresql_api +
data) which blindly concatenates explain_postgresql_api and the value returned
from send_post_request; instead detect what send_post_request returned and build
an explicit payload: if send_post_request returned a redirect target (e.g., a
string starting with '/' or a relative path) join it with explain_postgresql_api
using a proper URL join (urllib.parse.urljoin) and return that as a 'redirect'
or 'url' field; if it returned a 200 body (non-URL), return it as a 'body' or
'result' field without concatenation; update the success branch in this function
(and the similar block around lines 184-200) to call
make_json_response(success=1, url=full_url) or make_json_response(success=1,
result=body) as appropriate rather than concatenating strings.

155-181: ⚠️ Potential issue | 🔴 Critical

Block internal hosts in URL validation.

is_valid_url() only checks for http/https plus a hostname, so a user-controlled explain_postgresql_api can still target localhost, RFC1918, link-local, or metadata endpoints. That leaves these routes open to SSRF against internal services.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/__init__.py` around lines 155 - 181, Update is_valid_url
to block localhost and internal IP ranges: after parsing with urlparse and
extracting parsed.hostname, immediately reject hostname values like 'localhost'
and names ending with '.local'; then try to treat hostname as an IP literal
using ipaddress.ip_address and return False if ip.is_private or ip.is_loopback
or ip.is_link_local or ip.is_reserved or ip.is_multicast; if hostname is not an
IP literal, call socket.getaddrinfo(hostname, None) and for each resolved
address use ipaddress.ip_address to ensure none are in those disallowed ranges
(return False if any are); on any resolution or parsing exception return False
and otherwise return True. Use the existing is_valid_url function and the
parsed.hostname symbol, and import ipaddress and socket.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@web/pgadmin/tools/ep/__init__.py`:
- Around line 193-195: The opener still follows redirects because
HTTPRedirectHandler is present by default; update the code that builds
no302opener (used by send_post_request) to include a custom HTTPRedirectHandler
that disables redirects (e.g., subclass urllib.request.HTTPRedirectHandler and
override redirect_request to return None) so the 302 response and Location
header are preserved, then use that custom handler in build_opener in place of
the current No302HTTPErrorProcessor-only opener; apply the same change to the
other opener creation mentioned around the 204-227 block so both places stop
following redirects before your processor sees them.
- Around line 146-152: The handler currently returns
make_json_response(success=1, data=explain_postgresql_api + data) which blindly
concatenates explain_postgresql_api and the value returned from
send_post_request; instead detect what send_post_request returned and build an
explicit payload: if send_post_request returned a redirect target (e.g., a
string starting with '/' or a relative path) join it with explain_postgresql_api
using a proper URL join (urllib.parse.urljoin) and return that as a 'redirect'
or 'url' field; if it returned a 200 body (non-URL), return it as a 'body' or
'result' field without concatenation; update the success branch in this function
(and the similar block around lines 184-200) to call
make_json_response(success=1, url=full_url) or make_json_response(success=1,
result=body) as appropriate rather than concatenating strings.
- Around line 155-181: Update is_valid_url to block localhost and internal IP
ranges: after parsing with urlparse and extracting parsed.hostname, immediately
reject hostname values like 'localhost' and names ending with '.local'; then try
to treat hostname as an IP literal using ipaddress.ip_address and return False
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved or
ip.is_multicast; if hostname is not an IP literal, call
socket.getaddrinfo(hostname, None) and for each resolved address use
ipaddress.ip_address to ensure none are in those disallowed ranges (return False
if any are); on any resolution or parsing exception return False and otherwise
return True. Use the existing is_valid_url function and the parsed.hostname
symbol, and import ipaddress and socket.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 99951be3-52bb-4d6a-8083-3572aaa01128

📥 Commits

Reviewing files that changed from the base of the PR and between 28fe939 and 4653a9f.

📒 Files selected for processing (2)
  • web/pgadmin/tools/ep/__init__.py
  • web/pgadmin/tools/ep/static/js/ExplainPostgreSQL/formatSQL.js

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
web/pgadmin/tools/ep/__init__.py (2)

158-182: ⚠️ Potential issue | 🔴 Critical

is_valid_url() still permits SSRF to internal hosts.

This validator only constrains scheme/hostname, so values like http://localhost:..., http://192.168.x.x, or http://169.254.169.254 still pass and can be reached server-side through these endpoints. Please either restrict this setting to admin-controlled config or resolve A/AAAA records here and reject any non-global address before returning True.


225-230: ⚠️ Potential issue | 🟠 Major

The opener still follows redirects before you can read Location.

build_opener() still adds the default HTTPRedirectHandler here, because this call does not supply a replacement redirect handler. That means the 302 branch in send_post_request() is not dependable, so the /explain flow can end up following the redirect instead of returning the redirect target.

Proposed fix
+class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
+    def http_error_302(self, req, fp, code, msg, headers):
+        return fp
+
+    http_error_301 = http_error_303 = http_error_307 = http_error_308 = http_error_302
+
 # Build opener without HTTPRedirectHandler so No302HTTPErrorProcessor can handle 302 responses
 no302opener = urllib.request.build_opener(
+    NoRedirectHandler(),
     No302HTTPErrorProcessor(),
-    urllib.request.HTTPHandler(),
-    urllib.request.HTTPSHandler()
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/__init__.py` around lines 225 - 230, The opener
construction still lets the default HTTPRedirectHandler get inserted, so replace
the implicit redirect handler with an explicit no-redirect handler when creating
no302opener: define or use a subclass of urllib.request.HTTPRedirectHandler that
prevents redirects (e.g., overrides redirect_request to return None) and pass
that handler into urllib.request.build_opener alongside No302HTTPErrorProcessor,
so send_post_request() can reliably inspect 302 responses and Location headers
instead of having redirects followed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/pgadmin/tools/ep/__init__.py`:
- Around line 87-93: Change the request.get_json() call to use silent=True so
malformed or missing JSON returns None instead of raising BadRequest; update the
checks around request.get_json() (the call in this module, e.g., where data =
request.get_json()) to use data = request.get_json(silent=True) and keep the
existing isinstance(data, dict) guard and make_json_response() error path; apply
the same change to the other get_json() usage in this file where you perform the
same validation flow.

---

Duplicate comments:
In `@web/pgadmin/tools/ep/__init__.py`:
- Around line 225-230: The opener construction still lets the default
HTTPRedirectHandler get inserted, so replace the implicit redirect handler with
an explicit no-redirect handler when creating no302opener: define or use a
subclass of urllib.request.HTTPRedirectHandler that prevents redirects (e.g.,
overrides redirect_request to return None) and pass that handler into
urllib.request.build_opener alongside No302HTTPErrorProcessor, so
send_post_request() can reliably inspect 302 responses and Location headers
instead of having redirects followed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f301e112-8d4f-4121-9be9-4d0098c79cd1

📥 Commits

Reviewing files that changed from the base of the PR and between 4653a9f and f9e341b.

📒 Files selected for processing (1)
  • web/pgadmin/tools/ep/__init__.py

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
web/pgadmin/tools/ep/__init__.py (1)

158-185: ⚠️ Potential issue | 🟠 Major

SSRF risk: Private IPs and localhost are still allowed.

The docstring explicitly states that localhost and private IP ranges are allowed, but this creates SSRF exposure. Any authenticated user can configure explain_postgresql_api to target internal services (e.g., http://localhost:5000, http://192.168.1.1, or cloud metadata at http://169.254.169.254).

Consider one of:

  1. Block private IP ranges, localhost, and link-local addresses using the ipaddress module
  2. Make the API URL an admin-only configuration rather than a per-user preference
  3. Maintain a server-side allowlist of trusted EP endpoints

This relates to the previous unaddressed review comment about incomplete SSRF protection.

🛡️ Suggested implementation to block private IPs
+import socket
+import ipaddress
+
 def is_valid_url(url):
     """
-    Validate that a URL is safe to use (HTTP/HTTPS only, localhost and private IP ranges are allowed).
+    Validate that a URL is safe to use (HTTP/HTTPS only, no private/localhost targets).
     
     Args:
         url: The URL to validate
         
     Returns:
-        bool: True if URL is valid, False otherwise
+        bool: True if URL is valid and safe, False otherwise
     """
     if not url:
         return False
     
     try:
         parsed = urlparse(url)

         # Only allow http and https schemes
         if parsed.scheme not in ('http', 'https'):
             return False

         hostname = parsed.hostname
         if not hostname:
             return False

+        # Block localhost variants
+        if hostname.lower() in ('localhost', '127.0.0.1', '::1', '0.0.0.0'):
+            return False
+
+        # Resolve and check for private/internal IPs
+        try:
+            for info in socket.getaddrinfo(hostname, None):
+                ip = ipaddress.ip_address(info[4][0])
+                if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
+                    return False
+        except (socket.gaierror, ValueError):
+            return False
+
         return True
     except Exception:
         return False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/__init__.py` around lines 158 - 185, The is_valid_url
function currently permits localhost and private IPs; modify is_valid_url to
resolve parsed.hostname (use socket.getaddrinfo) and for every returned IP
convert to an ipaddress.ip_address and reject the URL if any address is
loopback, private, link-local, or otherwise reserved (e.g., is_loopback,
is_private, is_link_local, is_reserved); keep the existing scheme and hostname
checks but add these network checks for both IPv4 and IPv6, handle DNS
resolution errors by returning False, and import/use the socket and ipaddress
modules to perform the validation.
🧹 Nitpick comments (3)
web/pgadmin/tools/ep/__init__.py (3)

207-224: Inconsistent indentation: 2-space instead of 4-space.

Same indentation inconsistency as send_post_request. The class methods should use 4-space indentation to match the rest of the file and PEP 8.

♻️ Proposed fix
 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
+    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
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/__init__.py` around lines 207 - 224, The
No302HTTPErrorProcessor class uses 2-space indentation for its body and methods
(http_response and https_response) which is inconsistent with the rest of the
file and PEP8; update the class definition and its methods to use 4-space
indentation (match the style used by send_post_request) so the class body,
method signatures, and method bodies are indented with four spaces.

187-206: Inconsistent indentation: 2-space instead of 4-space.

This function uses 2-space indentation while the rest of the file uses 4-space indentation (PEP 8 standard). This was partially addressed from tabs, but the spacing is still inconsistent.

♻️ Proposed fix to normalize indentation
 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, timeout=10) 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)
+    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, timeout=10) 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)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/__init__.py` around lines 187 - 206, The
send_post_request function uses 2-space indentation which is inconsistent with
the file's 4-space PEP8 style; update the whole function block (function def
send_post_request, its try/except, the with no302opener.open(...) block, and all
nested statements including response handling and return branches) to use 4
spaces per indentation level so it matches the rest of the module and preserves
existing logic (urllib.request.Request, no302opener.open, response.code check,
parse handling, and the exception return).

252-254: Silent exception swallowing may hide configuration issues.

The try-except-pass pattern silently discards any exceptions during preference retrieval. While returning None as a fallback is reasonable, completely suppressing errors could mask underlying configuration problems.

Consider logging the exception at debug level for troubleshooting:

♻️ Proposed fix
+from flask import current_app
+
 def get_preference_value(name):
     ...
     try:
         pref_module = Preferences.module(MODULE_NAME)
         if pref_module:
             pref = pref_module.preference(name)
             if pref:
                 value = pref.get()
                 if isinstance(value, str):
                     value = value.strip()
                     return value or None
                 return value
-    except Exception:
-        pass
+    except Exception as e:
+        current_app.logger.debug(
+            f"Failed to retrieve preference '{name}': {e}"
+        )
     return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/pgadmin/tools/ep/__init__.py` around lines 252 - 254, The silent except
block in __init__.py that currently does "except Exception: pass" before "return
None" should log the caught exception at debug (or debug/error) level so
configuration issues aren't hidden; update the exception handler in that
preference retrieval block to catch Exception as e and call the module/app
logger (e.g., logger.debug or current_app.logger.debug) with a short contextual
message and the exception (or its traceback), then return None as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@web/pgadmin/tools/ep/__init__.py`:
- Around line 158-185: The is_valid_url function currently permits localhost and
private IPs; modify is_valid_url to resolve parsed.hostname (use
socket.getaddrinfo) and for every returned IP convert to an ipaddress.ip_address
and reject the URL if any address is loopback, private, link-local, or otherwise
reserved (e.g., is_loopback, is_private, is_link_local, is_reserved); keep the
existing scheme and hostname checks but add these network checks for both IPv4
and IPv6, handle DNS resolution errors by returning False, and import/use the
socket and ipaddress modules to perform the validation.

---

Nitpick comments:
In `@web/pgadmin/tools/ep/__init__.py`:
- Around line 207-224: The No302HTTPErrorProcessor class uses 2-space
indentation for its body and methods (http_response and https_response) which is
inconsistent with the rest of the file and PEP8; update the class definition and
its methods to use 4-space indentation (match the style used by
send_post_request) so the class body, method signatures, and method bodies are
indented with four spaces.
- Around line 187-206: The send_post_request function uses 2-space indentation
which is inconsistent with the file's 4-space PEP8 style; update the whole
function block (function def send_post_request, its try/except, the with
no302opener.open(...) block, and all nested statements including response
handling and return branches) to use 4 spaces per indentation level so it
matches the rest of the module and preserves existing logic
(urllib.request.Request, no302opener.open, response.code check, parse handling,
and the exception return).
- Around line 252-254: The silent except block in __init__.py that currently
does "except Exception: pass" before "return None" should log the caught
exception at debug (or debug/error) level so configuration issues aren't hidden;
update the exception handler in that preference retrieval block to catch
Exception as e and call the module/app logger (e.g., logger.debug or
current_app.logger.debug) with a short contextual message and the exception (or
its traceback), then return None as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7ba03690-bc46-4eba-9ab2-2547957fde70

📥 Commits

Reviewing files that changed from the base of the PR and between f9e341b and 117e25c.

📒 Files selected for processing (1)
  • web/pgadmin/tools/ep/__init__.py

@dpage
Copy link
Contributor

dpage commented Mar 11, 2026

[With my pgAdmin hat on, and NOT my PGCA one] Can you confirm that explain-postgresql.com has appropriate licensing for use of the domain name including a community trademark, or considers it fair use (and if so, under what part of the trademark policy)? As licensees ourselves, we have to be careful about using any non-compliant services.

We also need to consider whether we actually want to add support for 3rd party services such as this - we've only used services from large PostgreSQL vendors in the past, and this one is quite different in that it is effectively sending schema/query data to a 3rd party, unlike the others which call APIs upon specific user request to create a service (e.g. an RDS instance)

On the patch itself, I note:

  • The "Explain PostgreSQL" tab opens on every EXPLAIN query regardless of the preference setting — no check for explain_postgresql preference before opening the tab and making the external API call
  • Query plans and SQL are sent to a third-party service by default with no consent — both explain_postgresql and explain_postgresql_format preferences default to True
  • The new tab auto-focuses, stealing attention from the built-in Explain tab — the true flag in openTab forces focus away from the existing Explain panel on every query
  • iframe with allow-scripts + allow-same-origin sandbox effectively negates the sandbox — a malicious/compromised endpoint could access pgAdmin's session
  • Module name "ep" is cryptic — other tools use descriptive names like sqleditor, schema_diff
  • 10-second hang with no UI feedback when formatting API is unreachable — user sees nothing while waiting before fallback to local formatter
  • send_post_request parse parameter is dead code — never called with parse=True
  • <p>Loading...</p> instead of pgAdmin's standard Loader component for the explain panel loading state
  • Trailing whitespace / inconsistent formatting in init.py
  • No tests for the new module
  • There are no documentation or screenshot updates

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants