From c08c82f4ca3369e55b81a4147c73b82eaba9231c Mon Sep 17 00:00:00 2001 From: Kundan Sable Date: Wed, 21 Oct 2026 11:18:01 +0530 Subject: [PATCH 1/3] Fix the Statistics node failing to list its children in Object Explorer. #10310 pgAdmin renders its templates with trim_blocks, which removes the newline after a block tag. nodes.sql ended its WHERE clause with a conditional, so with neither a statistics id nor schema_diff set, which is what expanding the Statistics collection sends, the SQL ran into "::oidORDER BY" and the server rejected it. Give each condition its own line ending in plain text, and add a test that renders the template for every parameter combination and runs it. Co-Authored-By: Claude Sonnet 5.5 --- .../statistics/sql/default/nodes.sql | 8 +- .../tests/test_statistics_nodes_template.py | 83 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_nodes_template.py diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql index 7347a63c6a3..b99cecae04a 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql @@ -9,7 +9,13 @@ FROM pg_catalog.pg_statistic_ext s ON (des.objoid = s.oid AND des.classoid = 'pg_statistic_ext'::regclass) {### Without scid, as after an update that may have moved the object to ###} {### another schema, the object is looked up by its OID alone ###} -WHERE {% if scid %}s.stxnamespace = {{scid}}::oid{% else %}true{% endif %} +{### pgAdmin renders templates with trim_blocks, which removes the newline ###} +{### after a block tag, so each condition below is its own line ending in ###} +{### plain text; otherwise the last one would run into ORDER BY ###} +WHERE true +{% if scid %} + AND s.stxnamespace = {{scid}}::oid +{% endif %} {% if stid %} AND s.oid = {{stid}}::oid {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_nodes_template.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_nodes_template.py new file mode 100644 index 00000000000..afcb93413f4 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_nodes_template.py @@ -0,0 +1,83 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2026, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Tests for the statistics nodes.sql template (#10310). + +pgAdmin renders its SQL templates with Jinja's trim_blocks enabled, which +removes the newline after every block tag. nodes.sql used to end its WHERE +clause with a conditional, so with neither a statistics id nor schema_diff +set - which is exactly what the Object Explorer sends when it expands the +Statistics collection - the rendered SQL read "...::oidORDER BY s.stxname" +and the server rejected it, leaving the node empty. + +The template is rendered through the application's own Jinja environment, +via the versioned template loader, and executed against the server. +""" + +import re + +from flask import render_template + +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils + +# A keyword that follows a value with no whitespace between them. +GLUED_KEYWORD = re.compile(r'[^\s](ORDER BY|GROUP BY|LIMIT)\b') + + +class StatisticsNodesTemplateTestCase(BaseTestGenerator): + """nodes.sql must be valid SQL for every combination it is rendered with.""" + + scenarios = [ + ('Listing the statistics of a schema (Object Explorer)', + dict(params=dict(scid=2200), expect_in=['s.stxnamespace = 2200'])), + ('Looking one object up in its schema', + dict(params=dict(scid=2200, stid=1), + expect_in=['s.stxnamespace = 2200', 's.oid = 1'])), + ('Looking one object up by OID alone', + dict(params=dict(stid=1), expect_in=['s.oid = 1'])), + ('Listing a schema for Schema Diff', + dict(params=dict(scid=2200, schema_diff=True), + expect_in=['s.stxnamespace = 2200', 'deptype = \'e\''])), + ] + + def setUp(self): + # No pgAdmin server registration is needed: this exercises the SQL, + # so it talks to the server directly. + self.connection = utils.get_db_connection( + self.server['db'], self.server['username'], + self.server['db_password'], self.server['host'], + self.server['port'], self.server['sslmode']) + cursor = self.connection.cursor() + cursor.execute("SELECT current_setting('server_version_num')::int") + self.server_version = cursor.fetchone()[0] + + def tearDown(self): + self.connection.close() + + def runTest(self): + if self.server_version < 140000: + self.skipTest('Extended statistics objects in the tree need ' + 'PostgreSQL 14 or later.') + + template_path = 'statistics/sql/#{0}#'.format(self.server_version) + with self.app.app_context(): + sql = render_template( + "/".join([template_path, 'nodes.sql']), **self.params) + + self.assertIsNone( + GLUED_KEYWORD.search(sql), + 'A clause has run into the keyword after it:\n%s' % sql) + for fragment in self.expect_in: + self.assertIn(fragment, sql) + + # The real test of "valid SQL" is the server accepting it. + cursor = self.connection.cursor() + cursor.execute(sql) + cursor.fetchall() From 47a678c472553b955899fe7c38073baaeea55765 Mon Sep 17 00:00:00 2001 From: Kundan Sable Date: Wed, 21 Oct 2026 15:28:46 +0530 Subject: [PATCH 2/3] Fix the line length in the statistics nodes template test. #10310 Co-Authored-By: Claude Sonnet 5.5 --- .../statistics/templates/statistics/sql/default/nodes.sql | 8 +------- .../statistics/tests/test_statistics_nodes_template.py | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql index b99cecae04a..7347a63c6a3 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql @@ -9,13 +9,7 @@ FROM pg_catalog.pg_statistic_ext s ON (des.objoid = s.oid AND des.classoid = 'pg_statistic_ext'::regclass) {### Without scid, as after an update that may have moved the object to ###} {### another schema, the object is looked up by its OID alone ###} -{### pgAdmin renders templates with trim_blocks, which removes the newline ###} -{### after a block tag, so each condition below is its own line ending in ###} -{### plain text; otherwise the last one would run into ORDER BY ###} -WHERE true -{% if scid %} - AND s.stxnamespace = {{scid}}::oid -{% endif %} +WHERE {% if scid %}s.stxnamespace = {{scid}}::oid{% else %}true{% endif %} {% if stid %} AND s.oid = {{stid}}::oid {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_nodes_template.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_nodes_template.py index afcb93413f4..bb4ab468085 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_nodes_template.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_nodes_template.py @@ -32,7 +32,7 @@ class StatisticsNodesTemplateTestCase(BaseTestGenerator): - """nodes.sql must be valid SQL for every combination it is rendered with.""" + """nodes.sql must be valid SQL for every way it is rendered.""" scenarios = [ ('Listing the statistics of a schema (Object Explorer)', From ee04832e25ab77f283ab80eb0139a26a8d812f72 Mon Sep 17 00:00:00 2001 From: Kundan Sable Date: Wed, 21 Oct 2026 15:29:26 +0530 Subject: [PATCH 3/3] Restore the nodes.sql fix reverted by the previous commit. #10310 The previous commit accidentally reverted the template change along with the line-length fix. Put it back. Co-Authored-By: Claude Sonnet 5.5 --- .../statistics/templates/statistics/sql/default/nodes.sql | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql index 7347a63c6a3..b99cecae04a 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql @@ -9,7 +9,13 @@ FROM pg_catalog.pg_statistic_ext s ON (des.objoid = s.oid AND des.classoid = 'pg_statistic_ext'::regclass) {### Without scid, as after an update that may have moved the object to ###} {### another schema, the object is looked up by its OID alone ###} -WHERE {% if scid %}s.stxnamespace = {{scid}}::oid{% else %}true{% endif %} +{### pgAdmin renders templates with trim_blocks, which removes the newline ###} +{### after a block tag, so each condition below is its own line ending in ###} +{### plain text; otherwise the last one would run into ORDER BY ###} +WHERE true +{% if scid %} + AND s.stxnamespace = {{scid}}::oid +{% endif %} {% if stid %} AND s.oid = {{stid}}::oid {% endif %}