From bc1825eb29387c02314aeb4540c35e47ea69f620 Mon Sep 17 00:00:00 2001 From: Murtuza Zabuawala Date: Tue, 3 Feb 2026 15:52:12 +0530 Subject: [PATCH 1/2] Feat: Added a Statistics node --- .../servers/databases/schemas/__init__.py | 3 + .../databases/schemas/statistics/__init__.py | 918 ++++++++++++++++++ .../statistics/static/img/coll-statistics.svg | 1 + .../statistics/static/img/statistics.svg | 1 + .../statistics/static/js/statistics.js | 120 +++ .../statistics/static/js/statistics.ui.js | 275 ++++++ .../statistics/sql/16_plus/create.sql | 14 + .../statistics/sql/16_plus/properties.sql | 40 + .../statistics/sql/16_plus/update.sql | 31 + .../statistics/sql/17_plus/create.sql | 14 + .../statistics/sql/17_plus/properties.sql | 40 + .../statistics/sql/17_plus/update.sql | 31 + .../sql/default/backend_support.sql | 5 + .../statistics/sql/default/coll_stats.sql | 5 + .../statistics/sql/default/count.sql | 4 + .../statistics/sql/default/create.sql | 14 + .../statistics/sql/default/delete.sql | 12 + .../statistics/sql/default/get_oid.sql | 6 + .../statistics/sql/default/nodes.sql | 17 + .../statistics/sql/default/properties.sql | 40 + .../statistics/sql/default/stats.sql | 30 + .../statistics/sql/default/update.sql | 24 + .../schemas/statistics/tests/__init__.py | 0 .../statistics/tests/pg/14_plus/__init__.py | 0 .../schemas/statistics/tests/pg/__init__.py | 0 .../tests/statistics_test_data.json | 250 +++++ .../statistics/tests/test_statistics_add.py | 121 +++ .../tests/test_statistics_delete.py | 159 +++ .../tests/test_statistics_delete_multiple.py | 125 +++ .../statistics/tests/test_statistics_get.py | 150 +++ .../statistics/tests/test_statistics_put.py | 126 +++ .../schemas/statistics/tests/utils.py | 332 +++++++ web/webpack.config.js | 1 + web/webpack.shim.js | 1 + 34 files changed, 2910 insertions(+) create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/__init__.py create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/img/coll-statistics.svg create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/img/statistics.svg create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/js/statistics.js create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/js/statistics.ui.js create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/create.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/properties.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/update.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/create.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/properties.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/update.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/backend_support.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/coll_stats.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/count.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/create.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/delete.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/get_oid.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/properties.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/stats.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/update.sql create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/__init__.py create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/pg/14_plus/__init__.py create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/pg/__init__.py create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/statistics_test_data.json create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_add.py create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_delete.py create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_delete_multiple.py create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_get.py create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_put.py create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/utils.py diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/__init__.py index 697183d17e1..33997795736 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/__init__.py @@ -157,6 +157,9 @@ def register(self, app, options): from .sequences import blueprint as module self.submodules.append(module) + from .statistics import blueprint as module + self.submodules.append(module) + from .synonyms import blueprint as module self.submodules.append(module) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/__init__.py new file mode 100644 index 00000000000..d0428b39476 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/__init__.py @@ -0,0 +1,918 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2025, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Implements Statistics Node""" + +from functools import wraps +import json + +from flask import render_template, request, jsonify +from flask_babel import gettext as _ + +import pgadmin.browser.server_groups.servers.databases as database +from config import PG_DEFAULT_DRIVER +from pgadmin.browser.server_groups.servers.databases.schemas.utils import \ + SchemaChildModule +from pgadmin.browser.utils import PGChildNodeView +from pgadmin.tools.schema_diff.compare import SchemaDiffObjectCompare +from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry +from pgadmin.utils.ajax import make_json_response, internal_server_error, \ + make_response as ajax_response, gone +from pgadmin.utils.driver import get_driver + + +class StatisticsModule(SchemaChildModule): + """ + class StatisticsModule(SchemaChildModule) + + A module class for Statistics node derived from + SchemaChildModule. + + Methods: + ------- + * __init__(*args, **kwargs) + - Method is used to initialize the StatisticsModule and its base module. + + * get_nodes(gid, sid, did, scid) + - Method is used to generate the browser collection node. + + * script_load() + - Load the module script for statistics, when any of the database node + is initialized. + + * node_inode() + - Method is overridden from its base class to make the node as leaf node. + + """ + + _NODE_TYPE = 'statistics' + _COLLECTION_LABEL = _("Statistics") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.min_ver = 140000 # Only current official supported versions + self.max_ver = None + + def get_nodes(self, gid, sid, did, scid): + """ + Generate the statistics node + """ + if self.has_nodes( + sid, + did, + scid=scid, + base_template_path=StatisticsView.BASE_TEMPLATE_PATH): + yield self.generate_browser_collection_node(scid) + + @property + def script_load(self): + """ + Load the module script for database, when any of the database node is + initialized. + """ + return database.DatabaseModule.node_type + + @property + def node_inode(self): + """ + Override this property to make the node a leaf node. + + Returns: False as this is the leaf node + """ + return False + + +blueprint = StatisticsModule(__name__) + + +class StatisticsView(PGChildNodeView, SchemaDiffObjectCompare): + """ + This class is responsible for generating routes for Extended Statistics + node. + + Methods: + ------- + * list() + - This function returns all statistics nodes within a schema. + + * nodes() + - This function returns all statistics nodes as tree nodes. + + * properties() + - This function shows the properties of a selected statistics node. + + * create() + - This function creates a new statistics object. + + * update() + - This function updates a statistics object. + + * delete() + - This function deletes a statistics object. + + * sql() + - This function returns the SQL for the selected statistics object. + + * msql() + - This function returns the modified SQL. + + * statistics() + - This function returns statistics information. + + * dependencies() + - This function returns the dependencies for the selected statistics. + + * dependents() + - This function returns the dependents for the selected statistics. + + """ + + node_type = blueprint.node_type + node_label = "Statistics" + node_icon = "icon-%s" % node_type + BASE_TEMPLATE_PATH = 'statistics/sql/#{0}#' + + parent_ids = [ + {'type': 'int', 'id': 'gid'}, + {'type': 'int', 'id': 'sid'}, + {'type': 'int', 'id': 'did'}, + {'type': 'int', 'id': 'scid'} + ] + ids = [ + {'type': 'int', 'id': 'stid'} + ] + + operations = dict({ + 'obj': [ + {'get': 'properties', 'delete': 'delete', 'put': 'update'}, + {'get': 'list', 'post': 'create', 'delete': 'delete'} + ], + 'delete': [{'delete': 'delete'}, {'delete': 'delete'}], + 'children': [{'get': 'children'}], + 'nodes': [{'get': 'nodes'}, {'get': 'nodes'}], + 'sql': [{'get': 'sql'}], + 'msql': [{'get': 'msql'}, {'get': 'msql'}], + 'stats': [{'get': 'statistics'}, {'get': 'statistics'}], + 'dependency': [{'get': 'dependencies'}], + 'dependent': [{'get': 'dependents'}] + }) + + keys_to_ignore = ['oid', 'oid-2', 'schemaoid', 'tableoid'] + + _PROPERTIES_SQL = 'properties.sql' + _NODES_SQL = 'nodes.sql' + _CREATE_SQL = 'create.sql' + _UPDATE_SQL = 'update.sql' + _DELETE_SQL = 'delete.sql' + _OID_SQL = 'get_oid.sql' + _STATS_SQL = 'stats.sql' + _COLL_STATS_SQL = 'coll_stats.sql' + + def check_precondition(action=None): + """ + This function will behave as a decorator which will check + database connection before running view, it will also attach + manager, conn & template_path properties to self + """ + + def wrap(f): + @wraps(f) + def wrapped(self, *args, **kwargs): + + driver = get_driver(PG_DEFAULT_DRIVER) + self.manager = driver.connection_manager(kwargs['sid']) + + if action and action in ["drop"]: + self.conn = self.manager.connection() + elif 'did' in kwargs: + self.conn = self.manager.connection(did=kwargs['did']) + else: + self.conn = self.manager.connection() + + self.datistemplate = False + if ( + self.manager.db_info is not None and + kwargs['did'] in self.manager.db_info and + 'datistemplate' in self.manager.db_info[kwargs['did']] + ): + self.datistemplate = self.manager.db_info[ + kwargs['did']]['datistemplate'] + + self.template_path = self.BASE_TEMPLATE_PATH.format( + self.manager.version + ) + self.qtIdent = driver.qtIdent + + return f(self, *args, **kwargs) + return wrapped + return wrap + + @check_precondition(action='list') + def list(self, gid, sid, did, scid): + """ + This function returns all statistics nodes within a schema. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + scid: Schema ID + + Returns: + JSON of available statistics nodes + """ + SQL = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + scid=scid + ) + status, res = self.conn.execute_dict(SQL) + + if not status: + return internal_server_error(errormsg=res) + + return ajax_response( + response=res['rows'], + status=200 + ) + + @check_precondition(action='nodes') + def nodes(self, gid, sid, did, scid, stid=None): + """ + This function returns all statistics nodes as tree nodes. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + scid: Schema ID + stid: Statistics ID (optional) + + Returns: + JSON of available statistics nodes + """ + res = [] + + SQL = render_template( + "/".join([self.template_path, self._NODES_SQL]), + scid=scid, + stid=stid, + conn=self.conn + ) + status, rset = self.conn.execute_dict(SQL) + if not status: + return internal_server_error(errormsg=rset) + + if stid is not None: + if len(rset['rows']) == 0: + return gone(errormsg=self.not_found_error_msg()) + row = rset['rows'][0] + return make_json_response( + data=self.blueprint.generate_browser_node( + row['oid'], + scid, + row['name'], + icon=self.node_icon, + description=row['comment'] + ), + status=200 + ) + + for row in rset['rows']: + res.append( + self.blueprint.generate_browser_node( + row['oid'], + scid, + row['name'], + icon=self.node_icon, + description=row['comment'] + )) + + return make_json_response( + data=res, + status=200 + ) + + @check_precondition(action='properties') + def properties(self, gid, sid, did, scid, stid): + """ + This function shows the properties of the selected statistics node. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + scid: Schema ID + stid: Statistics ID + + Returns: + JSON of statistics properties + """ + status, res = self._fetch_properties(scid, stid) + if not status: + return res + + return ajax_response( + response=res, + status=200 + ) + + def _fetch_properties(self, scid, stid): + """ + This function is used to fetch the properties of the specified object + + Args: + scid: Schema ID + stid: Statistics ID + + Returns: + Tuple of (status, result) + """ + sql = render_template( + "/".join([self.template_path, self._PROPERTIES_SQL]), + scid=scid, stid=stid + ) + status, res = self.conn.execute_dict(sql) + + if not status: + return False, internal_server_error(errormsg=res) + elif len(res['rows']) == 0: + return False, gone(self.not_found_error_msg()) + + res['rows'][0]['is_sys_obj'] = ( + res['rows'][0]['oid'] <= self._DATABASE_LAST_SYSTEM_OID or + self.datistemplate) + + # Convert stat_types from raw array to list of names + row = res['rows'][0] + stat_types = [] + if row.get('has_ndistinct'): + stat_types.append('ndistinct') + if row.get('has_dependencies'): + stat_types.append('dependencies') + if row.get('has_mcv'): + stat_types.append('mcv') + row['stat_types'] = stat_types + + # Ensure columns is an array (convert None to empty array) + if row.get('columns') is None: + row['columns'] = [] + + # Set has_expressions boolean based on expressions field + # expressions is pg_node_tree (bytea) - we just check if it exists + row['has_expressions'] = row.get('expressions') is not None + + # Remove expressions from display (it's internal PostgreSQL format) + row.pop('expressions', None) + + # Ensure stattarget has a default value if None + if row.get('stattarget') is None: + row['stattarget'] = -1 + + # Format computed statistics values for display + # These come from pg_statistic_ext_data and are already in text format + if row.get('ndistinct_values'): + row['ndistinct_values'] = str(row['ndistinct_values']) + if row.get('dependencies_values'): + row['dependencies_values'] = str(row['dependencies_values']) + + return True, res['rows'][0] + + @check_precondition(action='create') + def create(self, gid, sid, did, scid): + """ + This function creates a new statistics object. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + scid: Schema ID + + Returns: + JSON response with the new statistics node + """ + data = request.form if request.form else json.loads( + request.data + ) + + # Name is optional in PostgreSQL 16+ (version 160000) + required_args = ['schema', 'table', 'stat_types'] + if self.manager.version < 160000: + required_args.insert(0, 'name') + + for arg in required_args: + is_arg = arg in data + is_str_arg = isinstance(data.get(arg), str) + is_arg_empty_str = is_str_arg and not data.get(arg).strip() + if not is_arg or is_arg_empty_str: + return make_json_response( + status=400, + success=0, + errormsg=_( + "Could not find the required parameter ({})." + ).format(arg) + ) + + # Parse expression_list into expressions array if provided + has_columns = 'columns' in data and len(data.get('columns', [])) > 0 + has_expression_list = 'expression_list' in data and data.get( + 'expression_list', '').strip() + + # Convert expression_list (comma-separated) to expressions array + if has_expression_list: + expr_list = data.get('expression_list', '').strip() + # Split by comma and strip whitespace from each expression + data['expressions'] = [ + {'expression': expr.strip()} + for expr in expr_list.split(',') + if expr.strip() + ] + has_expressions = len(data['expressions']) > 0 + else: + data['expressions'] = [] + has_expressions = False + + if not has_columns and not has_expressions: + return make_json_response( + status=400, + success=0, + errormsg=_( + "Either columns or expressions must be specified" + ) + ) + + # Validate minimum 2 items for multi-column statistics + # (not for single expression) + if has_columns and not has_expressions and \ + len(data.get('columns', [])) < 2: + return make_json_response( + status=400, + success=0, + errormsg=_( + "At least 2 columns must be specified " + "for multi-column statistics." + ) + ) + + # Validate at least 1 stat_type + if len(data.get('stat_types', [])) < 1: + return make_json_response( + status=400, + success=0, + errormsg=_( + "At least 1 statistics type must be selected." + ) + ) + + try: + # Generate CREATE STATISTICS SQL + sql = render_template( + "/".join([self.template_path, self._CREATE_SQL]), + data=data, conn=self.conn + ) + except Exception as e: + return internal_server_error(errormsg=str(e)) + + status, msg = self.conn.execute_scalar(sql) + if not status: + return internal_server_error(errormsg=msg) + + # Get OID of newly created statistics + # For PostgreSQL 16+, if name is not provided, it's auto-generated + if data.get('name'): + sql = render_template( + "/".join([self.template_path, self._OID_SQL]), + name=data['name'], + schema=data['schema'], + conn=self.conn + ) + sql = sql.strip('\n').strip(' ') + + status, rset = self.conn.execute_2darray(sql) + if not status: + return internal_server_error(errormsg=rset) + + row = rset['rows'][0] + return jsonify( + node=self.blueprint.generate_browser_node( + row['oid'], + scid, + data['name'], + icon=self.node_icon + ) + ) + else: + # For auto-generated names in PG 16+, we need to query for the + # latest statistics + # This is a limitation - we'll return success but + # can't return the node + return make_json_response( + success=1, + info=_( + "Statistics created successfully with auto-generated name" + ) + ) + + @check_precondition(action='delete') + def delete(self, gid, sid, did, scid, stid=None, only_sql=False): + """ + This function deletes the statistics object. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + scid: Schema ID + stid: Statistics ID + only_sql: Return SQL only if True + + Returns: + JSON response + """ + if stid is None: + data = request.form if request.form else json.loads( + request.data + ) + else: + data = {'ids': [stid]} + + # Check if CASCADE operation + cascade = self._check_cascade_operation() + + try: + for stid in data['ids']: + # Fetch statistics details first + sql = render_template( + "/".join([self.template_path, self._DELETE_SQL]), + stid=stid + ) + status, res = self.conn.execute_dict(sql) + if not status: + return internal_server_error(errormsg=res) + + elif not res['rows']: + return gone( + errormsg=self.not_found_error_msg() + ) + + # Generate DROP SQL + sql = render_template( + "/".join([self.template_path, self._DELETE_SQL]), + name=res['rows'][0]['name'], + schema=res['rows'][0]['schema'], + cascade=cascade, + conn=self.conn + ) + + if only_sql: + return sql + + status, res = self.conn.execute_scalar(sql) + if not status: + return internal_server_error(errormsg=res) + + return make_json_response( + success=1, + info=_("Statistics dropped") + ) + + except Exception as e: + return internal_server_error(errormsg=str(e)) + + @check_precondition(action='update') + def update(self, gid, sid, did, scid, stid): + """ + This function updates the statistics object. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + scid: Schema ID + stid: Statistics ID + + Returns: + JSON response with updated node + """ + data = request.form if request.form else json.loads( + request.data + ) + sql, _ = self.get_SQL(gid, sid, did, data, scid, stid) + + # Most probably this is due to error + if not isinstance(sql, str): + return sql + + sql = sql.strip('\n').strip(' ') + + status, res = self.conn.execute_scalar(sql) + if not status: + return internal_server_error(errormsg=res) + + # Fetch updated node info + sql = render_template( + "/".join([self.template_path, self._NODES_SQL]), + scid=scid, + stid=stid, + conn=self.conn + ) + status, rset = self.conn.execute_2darray(sql) + if not status: + return internal_server_error(errormsg=rset) + + if len(rset['rows']) == 0: + return gone(errormsg=self.not_found_error_msg()) + + row = rset['rows'][0] + + return jsonify( + node=self.blueprint.generate_browser_node( + stid, + scid, + row['name'], + icon=self.node_icon, + description=row['comment'] + ) + ) + + @check_precondition(action='msql') + def msql(self, gid, sid, did, scid, stid=None): + """ + This function returns modified SQL for the object. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + scid: Schema ID + stid: Statistics ID + """ + data = {} + for k, v in request.args.items(): + try: + # Comments should be taken as is + if k in ('comment',): + data[k] = v + else: + data[k] = json.loads(v) + except ValueError: + data[k] = v + + if stid is None: + # Schema is required, name is optional in PG 16+ + if 'schema' not in data: + return make_json_response( + status=400, + success=0, + errormsg=_( + "Could not find the required parameter (schema)." + ) + ) + + sql, _sql_name = self.get_SQL(gid, sid, did, data, scid, stid) + + # Most probably this is due to error + if not isinstance(sql, str): + return sql + + sql = sql.strip('\n').strip(' ') + if sql == '': + sql = "--modified SQL" + + return make_json_response( + data=sql, + status=200 + ) + + def get_SQL(self, gid, sid, did, data, scid, stid=None, + add_not_exists_clause=False): + """ + This function generates SQL from model data. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + data: Form data + scid: Schema ID + stid: Statistics ID + add_not_exists_clause: Add IF NOT EXISTS clause + + Returns: + Tuple of (SQL, name) + """ + if stid is not None: + # Update operation + if 'stattarget' in data: + try: + data['stattarget'] = int(data['stattarget']) + except (ValueError, TypeError): + return make_json_response( + status=400, + success=0, + errormsg=_( + "Statistics target must be an integer." + ) + ), None + + status, old_data = self._fetch_properties(scid, stid) + if not status: + return old_data, None + + # Remove keys that shouldn't be compared + for key in self.keys_to_ignore: + old_data.pop(key, None) + + sql = render_template( + "/".join([self.template_path, self._UPDATE_SQL]), + data=data, o_data=old_data, conn=self.conn + ) + return sql, data.get('name', old_data['name']) + else: + # Create operation + # Name is optional in PostgreSQL 16+ + sql = render_template( + "/".join([self.template_path, self._CREATE_SQL]), + data=data, conn=self.conn, + add_not_exists_clause=add_not_exists_clause + ) + return sql, data.get('name', '') + + @check_precondition(action='sql') + def sql(self, gid, sid, did, scid, stid, **kwargs): + """ + This function generates reverse engineered SQL for the object. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + scid: Schema ID + stid: Statistics ID + """ + status, res = self._fetch_properties(scid, stid) + if not status: + return res + + sql = render_template( + "/".join([self.template_path, self._CREATE_SQL]), + data=res, conn=self.conn, add_not_exists_clause=False + ) + + return ajax_response(response=sql) + + @check_precondition(action='stats') + def statistics(self, gid, sid, did, scid, stid=None): + """ + Returns statistics for a particular object. + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + scid: Schema ID + stid: Statistics ID (optional) + """ + if stid is not None: + # Individual statistics + sql = render_template( + "/".join([self.template_path, self._STATS_SQL]), + stid=stid, + conn=self.conn + ) + else: + # Collection statistics + sql = render_template( + "/".join([self.template_path, self._COLL_STATS_SQL]), + scid=scid, + conn=self.conn + ) + + status, res = self.conn.execute_dict(sql) + + if not status: + return internal_server_error(errormsg=res) + + return make_json_response( + data=res, + status=200 + ) + + @check_precondition(action='depend') + def dependencies(self, gid, sid, did, scid, stid): + """ + This function gets the dependencies for the selected statistics node + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + scid: Schema ID + stid: Statistics ID + """ + dependencies_result = self.get_dependencies( + self.conn, stid + ) + + return ajax_response( + response=dependencies_result, + status=200 + ) + + @check_precondition(action='dependent') + def dependents(self, gid, sid, did, scid, stid): + """ + This function gets the dependents for the selected statistics node + + Args: + gid: Server Group ID + sid: Server ID + did: Database ID + scid: Schema ID + stid: Statistics ID + """ + dependents_result = self.get_dependents( + self.conn, stid + ) + + return ajax_response( + response=dependents_result, + status=200 + ) + + @check_precondition(action='fetch_objects_to_compare') + def fetch_objects_to_compare(self, sid, did, scid): + """ + This function fetches the list of all statistics objects for + schema diff. + + Args: + sid: Server ID + did: Database ID + scid: Schema ID + """ + res = dict() + + SQL = render_template( + "/".join([self.template_path, self._NODES_SQL]), + scid=scid, + schema_diff=True + ) + status, rset = self.conn.execute_2darray(SQL) + if not status: + return internal_server_error(errormsg=rset) + + for row in rset['rows']: + status, data = self._fetch_properties(scid, row['oid']) + if status: + res[row['name']] = data + + return res + + def get_sql_from_diff(self, **kwargs): + """ + This function generates SQL from model data for schema diff. + + Args: + kwargs: Additional parameters + + Returns: + SQL string + """ + gid = kwargs.get('gid') + sid = kwargs.get('sid') + did = kwargs.get('did') + scid = kwargs.get('scid') + source_params = kwargs.get('source_params') + target_params = kwargs.get('target_params') + comp_status = kwargs.get('comp_status') + + if comp_status == 'source_only': + # Object exists in source only - generate CREATE + sql, _ = self.get_SQL(gid=gid, sid=sid, did=did, + data=source_params, scid=scid) + elif comp_status == 'target_only': + # Object exists in target only - generate DROP + sql = render_template( + "/".join([self.template_path, self._DELETE_SQL]), + name=target_params['name'], + schema=target_params['schema'], + cascade=False + ) + else: + # Object exists in both - generate ALTER + sql, _ = self.get_SQL(gid=gid, sid=sid, did=did, + data=source_params, scid=scid, + stid=target_params['oid']) + + return sql + + +SchemaDiffRegistry(blueprint.node_type, StatisticsView) +StatisticsView.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/img/coll-statistics.svg b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/img/coll-statistics.svg new file mode 100644 index 00000000000..207abf3da2d --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/img/coll-statistics.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/img/statistics.svg b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/img/statistics.svg new file mode 100644 index 00000000000..207abf3da2d --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/img/statistics.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/js/statistics.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/js/statistics.js new file mode 100644 index 00000000000..3aaffd910b6 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/js/statistics.js @@ -0,0 +1,120 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2025, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { getNodeAjaxOptions, getNodeListByName } from '../../../../../../../static/js/node_ajax'; +import StatisticsSchema from './statistics.ui'; + +define('pgadmin.node.statistics', [ + 'sources/gettext', 'sources/url_for', 'pgadmin.browser', + 'pgadmin.node.schema.dir/child', 'pgadmin.node.schema.dir/schema_child_tree_node', + 'pgadmin.browser.collection', +], function( + gettext, url_for, pgBrowser, schemaChild, schemaChildTreeNode +) { + + // Extend the browser's collection class for statistics collection + if (!pgBrowser.Nodes['coll-statistics']) { + pgBrowser.Nodes['coll-statistics'] = + pgBrowser.Collection.extend({ + node: 'statistics', + label: gettext('Statistics'), + type: 'coll-statistics', + columns: ['name', 'table', 'comment'], + hasStatistics: true, + canDrop: schemaChildTreeNode.isTreeItemOfChildOfSchema, + canDropCascade: schemaChildTreeNode.isTreeItemOfChildOfSchema, + }); + } + + // Extend the browser's node class for statistics node + if (!pgBrowser.Nodes['statistics']) { + pgBrowser.Nodes['statistics'] = schemaChild.SchemaChildNode.extend({ + type: 'statistics', + sqlAlterHelp: 'sql-alterstatistics.html', + sqlCreateHelp: 'sql-createstatistics.html', + dialogHelp: url_for('help.static', {'filename': 'statistics_dialog.html'}), + label: gettext('Statistics'), + collection_type: 'coll-statistics', + hasSQL: true, + hasDepends: true, + hasStatistics: false, + Init: function() { + /* Avoid multiple registration of menus */ + if (this.initialized) + return; + + this.initialized = true; + + pgBrowser.add_menus([{ + name: 'create_statistics_on_coll', + node: 'coll-statistics', + module: this, + applies: ['object', 'context'], + callback: 'show_obj_properties', + category: 'create', + priority: 4, + label: gettext('Statistics...'), + data: {action: 'create', check: true}, + enable: 'canCreate', + shortcut_preference: ['browser', 'sub_menu_create'], + }, { + name: 'create_statistics', + node: 'statistics', + module: this, + applies: ['object', 'context'], + callback: 'show_obj_properties', + category: 'create', + priority: 4, + label: gettext('Statistics...'), + data: {action: 'create', check: true}, + enable: 'canCreate', + shortcut_preference: ['browser', 'sub_menu_create'], + }, { + name: 'create_statistics_on_schema', + node: 'schema', + module: this, + applies: ['object', 'context'], + callback: 'show_obj_properties', + category: 'create', + priority: 4, + label: gettext('Statistics...'), + data: {action: 'create', check: false}, + enable: 'canCreate', + }]); + + }, + + getSchema: function(treeNodeInfo, itemNodeData) { + return new StatisticsSchema( + { + role: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData), + schema: ()=>getNodeListByName('schema', treeNodeInfo, itemNodeData, {}, (m)=>{ + // Exclude pg_* schemas + return !(m.label.match(/^pg_/)); + }), + getTables: (params)=>getNodeListByName('table', treeNodeInfo, itemNodeData, {urlParams: params, includeItemKeys: ['_id']}), + getColumns: (params)=>{ + return getNodeAjaxOptions('get_columns', pgBrowser.Nodes['table'], treeNodeInfo, itemNodeData, {urlParams: params, useCache:false}, (rows)=>{ + return rows.map((r)=>({ + 'value': r.name, + 'image': 'icon-column', + 'label': r.name, + })); + }); + } + }, + { + schema: itemNodeData.label, + } + ); + }, + }); + } + return pgBrowser.Nodes['statistics']; +}); diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/js/statistics.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/js/statistics.ui.js new file mode 100644 index 00000000000..7457476ac41 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/js/statistics.ui.js @@ -0,0 +1,275 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2025, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; +import { isEmptyString } from '../../../../../../../../static/js/validators'; + +export default class StatisticsSchema extends BaseUISchema { + constructor(fieldOptions={}, initValues={}) { + super({ + name: undefined, + oid: undefined, + schema: undefined, + table: undefined, + columns: [], + expression_list: undefined, + has_expressions: false, + stat_types: [], + stattarget: undefined, + owner: undefined, + comment: undefined, + is_sys_obj: undefined, + ...initValues, + }); + + this.fieldOptions = { + role: [], + schema: [], + getTables: null, + getColumns: null, + ...fieldOptions, + }; + this.allTablesOptions = []; + } + + getTableOid(tabName) { + // Fetch the table OID from table name + for(const t of this.allTablesOptions) { + if(t.label === tabName) { + return t._id; + } + } + } + + get idAttribute() { + return 'oid'; + } + + get baseFields() { + let obj = this; + return [ + { + id: 'name', + label: gettext('Name'), + type: 'text', + mode: ['properties', 'create', 'edit'], + helpMessage: gettext('Statistics name'), + }, + { + id: 'oid', + label: gettext('OID'), + type: 'text', + mode: ['properties'], + }, + { + id: 'owner', + label: gettext('Owner'), + type: 'select', + options: this.fieldOptions.role, + controlProps: { allowClear: false }, + mode: ['properties', 'create', 'edit'], + }, + { + id: 'schema', + label: gettext('Schema'), + type: 'select', + options: this.fieldOptions.schema, + controlProps: { allowClear: false }, + mode: ['create', 'edit'], + cache_node: 'database', + cache_level: 'database', + depChange: () => ({ table: null, columns: [] }), + }, + { + id: 'table', + label: gettext('Table'), + type: (state) => ({ + type: 'select', + options: state.schema + ? () => obj.fieldOptions.getTables({ schema: state.schema }) + : [], + optionsReloadBasis: state.schema, + }), + optionsLoaded: (res) => obj.allTablesOptions = res, + mode: ['properties', 'create'], + deps: ['schema'], + editable: false, + noEmpty: true, + helpMessage: gettext('Select the table for which to collect statistics.'), + depChange: (state) => { + if (!state.schema) { + return { table: null, columns: [] }; + } + }, + }, + { + id: 'columns', + label: gettext('Columns'), + type: (state)=>{ + let tid = obj.getTableOid(state.table); + return { + type: 'select', + options: state.table ? ()=>obj.fieldOptions.getColumns({tid: tid}) : [], + optionsReloadBasis: state.table, + }; + }, + deps: ['schema', 'table'], + mode: ['properties', 'create'], + editable: false, + controlProps: { + multiple: true, + allowClear: true, + }, + helpMessage: gettext('Select at least 2 columns for multi-column statistics'), + depChange: (state)=>{ + // Clear columns when table changes + if(!state.table) { + return { + columns: [], + }; + } + } + }, + { + id: 'stat_types', + label: gettext('Statistics types'), + type: 'select', + mode: ['properties', 'create'], + editable: false, + controlProps: { + multiple: true, + allowClear: false, + }, + options: [ + {label: gettext('N-distinct'), value: 'ndistinct'}, + {label: gettext('Dependencies'), value: 'dependencies'}, + {label: gettext('MCV (Most Common Values)'), value: 'mcv'}, + ], + noEmpty: true, + helpMessage: gettext('Select one or more statistics types to collect'), + }, + { + id: 'expression_list', + label: gettext('Expressions'), + type: 'text', + mode: ['create'], + group: gettext('Definition'), + helpMessage: gettext('Enter SQL expression(s) for expression-based statistics, separated by commas'), + }, + { + id: 'has_expressions', + label: gettext('Has expressions?'), + type: 'switch', + mode: ['properties'], + readonly: true, + disabled: true, + helpMessage: gettext('Indicates if this statistics object includes expression-based statistics'), + + }, + { + id: 'stattarget', + label: gettext('Statistics target'), + type: 'int', + mode: ['properties', 'edit'], + min: -1, + helpMessage: gettext('Set statistics target for this object'), + + }, + { + id: 'ndistinct_values', + label: gettext('N-Distinct coefficients'), + type: 'multiline', + mode: ['properties'], + readonly: true, + disabled: true, + group: gettext('Computed Statistics'), + helpMessage: gettext('N-distinct coefficients computed by ANALYZE'), + }, + { + id: 'dependencies_values', + label: gettext('Functional dependencies'), + type: 'multiline', + mode: ['properties'], + readonly: true, + disabled: true, + group: gettext('Computed Statistics'), + helpMessage: gettext('Functional dependency statistics computed by ANALYZE'), + }, + { + id: 'has_mcv_values', + label: gettext('Has MCV data?'), + type: 'switch', + mode: ['properties'], + readonly: true, + disabled: true, + group: gettext('Computed Statistics'), + helpMessage: gettext('Indicates if most-common values data is available for this statistics object'), + }, + { + id: 'comment', + label: gettext('Comment'), + type: 'multiline', + mode: ['properties', 'create', 'edit'], + }, + { + id: 'is_sys_obj', + label: gettext('System statistics?'), + cell:'boolean', + type: 'switch', + mode: ['properties'], + }, + ]; + } + + validate(state, setError) { + let errors = false; + + // Validate table is selected + if (isEmptyString(state.table) && !state.oid) { + setError('table', gettext('Table must be selected.')); + errors = true; + } else { + setError('table', null); + } + + // Validate columns or expressions are provided + const hasColumns = state.columns && state.columns.length > 0; + const hasExpressions = state.expression_list && state.expression_list.trim().length > 0; + + if (!hasColumns && !hasExpressions && !state.oid) { + setError('columns', gettext('Either columns or expressions must be specified.')); + setError('expression_list', gettext('Either columns or expressions must be specified.')); + errors = true; + } else { + // Validate minimum 2 columns for multi-column statistics (when no expressions) + if (hasColumns && !hasExpressions && state.columns.length < 2 && !state.oid) { + setError('columns', gettext('At least 2 columns must be selected for multi-column statistics.')); + errors = true; + } else { + setError('columns', null); + } + + // Clear expression_list error if either columns or expressions are provided + if (hasColumns || hasExpressions) { + setError('expression_list', null); + } + } + + // Validate at least one stat type + if (state.stat_types && state.stat_types.length === 0 && !state.oid) { + setError('stat_types', gettext('At least one statistics type must be selected.')); + errors = true; + } else { + setError('stat_types', null); + } + + return errors; + } +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/create.sql new file mode 100644 index 00000000000..d1c356f53f5 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/create.sql @@ -0,0 +1,14 @@ +{### SQL to create extended statistics object (PostgreSQL 16+) ###} +{### Supports expression-based statistics and optional name ###} +CREATE STATISTICS{% if data.name %}{% if add_not_exists_clause %} IF NOT EXISTS{% endif %} {{ conn|qtIdent(data.schema, data.name) }}{% endif %}{% if data.stat_types and data.stat_types|length > 0 %} + + ({% for stype in data.stat_types %}{{ stype }}{% if not loop.last %}, {% endif %}{% endfor %}){% endif %} + + ON {% if data.expressions and data.expressions|length > 0 %}{% for expr in data.expressions %}({{ expr.expression if expr.expression is defined else expr }}){% if not loop.last %}, {% endif %}{% endfor %}{% elif data.columns %}{% for col in data.columns %}{{ conn|qtIdent(col) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} + + FROM {{ conn|qtIdent(data.schema, data.table) }}; +{% if data.comment and data.name %} + +COMMENT ON STATISTICS {{ conn|qtIdent(data.schema, data.name) }} + IS {{ data.comment|qtLiteral(conn) }}; +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/properties.sql new file mode 100644 index 00000000000..1e38fd0d1f5 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/properties.sql @@ -0,0 +1,40 @@ +{### Query extended statistics properties from pg_statistic_ext (PostgreSQL 14+) ###} +{% if scid %} +SELECT + s.oid, + s.stxname AS name, + s.stxnamespace AS schemaoid, + ns.nspname AS schema, + s.stxrelid AS tableoid, + t.relname AS table, + pg_catalog.pg_get_userbyid(s.stxowner) AS owner, + s.stxkeys AS column_attnums, + (SELECT array_agg(a.attname ORDER BY a.attnum) + FROM pg_catalog.pg_attribute a + WHERE a.attrelid = s.stxrelid + AND a.attnum = ANY(s.stxkeys) + ) AS columns, + s.stxkind AS stat_types_raw, + CASE WHEN 'd' = ANY(s.stxkind) THEN true ELSE false END AS has_ndistinct, + CASE WHEN 'f' = ANY(s.stxkind) THEN true ELSE false END AS has_dependencies, + CASE WHEN 'm' = ANY(s.stxkind) THEN true ELSE false END AS has_mcv, + s.stxstattarget AS stattarget, +{### stxexprs added in PostgreSQL 14 for expression statistics ###} + s.stxexprs AS expressions, +{### Statistics data from pg_statistic_ext_data (PostgreSQL 12+) ###} + sd.stxdndistinct AS ndistinct_values, + sd.stxddependencies AS dependencies_values, + CASE WHEN sd.stxdmcv IS NOT NULL THEN true ELSE false END AS has_mcv_values, + des.description AS comment +FROM pg_catalog.pg_statistic_ext s + LEFT JOIN pg_catalog.pg_namespace ns ON ns.oid = s.stxnamespace + LEFT JOIN pg_catalog.pg_class t ON t.oid = s.stxrelid + LEFT JOIN pg_catalog.pg_statistic_ext_data sd ON sd.stxoid = s.oid + LEFT OUTER JOIN pg_catalog.pg_description des + ON (des.objoid = s.oid AND des.classoid = 'pg_statistic_ext'::regclass) +WHERE s.stxnamespace = {{scid}}::oid +{% if stid %} + AND s.oid = {{stid}}::oid +{% endif %} +ORDER BY s.stxname +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/update.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/update.sql new file mode 100644 index 00000000000..2e528fc01dd --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/update.sql @@ -0,0 +1,31 @@ +{### SQL to update extended statistics object (PostgreSQL 16+) ###} +{### Rename statistics ###} +{% if data.name and data.name != o_data.name %} +ALTER STATISTICS {% if data.schema and data.schema != o_data.schema %}{{ conn|qtIdent(o_data.schema, o_data.name) }}{% else %}{{ conn|qtIdent(o_data.schema, o_data.name) }}{% endif %} + + RENAME TO {{ conn|qtIdent(data.name) }}; + +{% endif %} +{### Change schema ###} +{% if data.schema and data.schema != o_data.schema %} +ALTER STATISTICS {{ conn|qtIdent(o_data.schema, data.name if data.name else o_data.name) }} + SET SCHEMA {{ conn|qtIdent(data.schema) }}; + +{% endif %} +{### Change owner ###} +{% if data.owner and data.owner != o_data.owner %} +ALTER STATISTICS {{ conn|qtIdent(data.schema if data.schema else o_data.schema, data.name if data.name else o_data.name) }} + OWNER TO {{ conn|qtIdent(data.owner) }}; + +{% endif %} +{### Set statistics target (PostgreSQL 13+) ###} +{% if data.stattarget is defined and data.stattarget != o_data.stattarget %} +ALTER STATISTICS {{ conn|qtIdent(data.schema if data.schema else o_data.schema, data.name if data.name else o_data.name) }} + SET STATISTICS {{ data.stattarget }}; + +{% endif %} +{### Update comment ###} +{% if data.comment is defined and data.comment != o_data.comment %} +COMMENT ON STATISTICS {{ conn|qtIdent(data.schema if data.schema else o_data.schema, data.name if data.name else o_data.name) }} + IS {% if data.comment %}{{ data.comment|qtLiteral(conn) }}{% else %}NULL{% endif %}; +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/create.sql new file mode 100644 index 00000000000..b8102738f57 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/create.sql @@ -0,0 +1,14 @@ +{### SQL to create extended statistics object (PostgreSQL 17+) ###} +{### Supports expression-based statistics and optional name ###} +CREATE STATISTICS{% if data.name %}{% if add_not_exists_clause %} IF NOT EXISTS{% endif %} {{ conn|qtIdent(data.schema, data.name) }}{% endif %}{% if data.stat_types and data.stat_types|length > 0 %} + + ({% for stype in data.stat_types %}{{ stype }}{% if not loop.last %}, {% endif %}{% endfor %}){% endif %} + + ON {% if data.expressions and data.expressions|length > 0 %}{% for expr in data.expressions %}({{ expr.expression if expr.expression is defined else expr }}){% if not loop.last %}, {% endif %}{% endfor %}{% elif data.columns %}{% for col in data.columns %}{{ conn|qtIdent(col) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} + + FROM {{ conn|qtIdent(data.schema, data.table) }}; +{% if data.comment and data.name %} + +COMMENT ON STATISTICS {{ conn|qtIdent(data.schema, data.name) }} + IS {{ data.comment|qtLiteral(conn) }}; +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/properties.sql new file mode 100644 index 00000000000..1e38fd0d1f5 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/properties.sql @@ -0,0 +1,40 @@ +{### Query extended statistics properties from pg_statistic_ext (PostgreSQL 14+) ###} +{% if scid %} +SELECT + s.oid, + s.stxname AS name, + s.stxnamespace AS schemaoid, + ns.nspname AS schema, + s.stxrelid AS tableoid, + t.relname AS table, + pg_catalog.pg_get_userbyid(s.stxowner) AS owner, + s.stxkeys AS column_attnums, + (SELECT array_agg(a.attname ORDER BY a.attnum) + FROM pg_catalog.pg_attribute a + WHERE a.attrelid = s.stxrelid + AND a.attnum = ANY(s.stxkeys) + ) AS columns, + s.stxkind AS stat_types_raw, + CASE WHEN 'd' = ANY(s.stxkind) THEN true ELSE false END AS has_ndistinct, + CASE WHEN 'f' = ANY(s.stxkind) THEN true ELSE false END AS has_dependencies, + CASE WHEN 'm' = ANY(s.stxkind) THEN true ELSE false END AS has_mcv, + s.stxstattarget AS stattarget, +{### stxexprs added in PostgreSQL 14 for expression statistics ###} + s.stxexprs AS expressions, +{### Statistics data from pg_statistic_ext_data (PostgreSQL 12+) ###} + sd.stxdndistinct AS ndistinct_values, + sd.stxddependencies AS dependencies_values, + CASE WHEN sd.stxdmcv IS NOT NULL THEN true ELSE false END AS has_mcv_values, + des.description AS comment +FROM pg_catalog.pg_statistic_ext s + LEFT JOIN pg_catalog.pg_namespace ns ON ns.oid = s.stxnamespace + LEFT JOIN pg_catalog.pg_class t ON t.oid = s.stxrelid + LEFT JOIN pg_catalog.pg_statistic_ext_data sd ON sd.stxoid = s.oid + LEFT OUTER JOIN pg_catalog.pg_description des + ON (des.objoid = s.oid AND des.classoid = 'pg_statistic_ext'::regclass) +WHERE s.stxnamespace = {{scid}}::oid +{% if stid %} + AND s.oid = {{stid}}::oid +{% endif %} +ORDER BY s.stxname +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/update.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/update.sql new file mode 100644 index 00000000000..830ea36f321 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/update.sql @@ -0,0 +1,31 @@ +{### SQL to update extended statistics object (PostgreSQL 17+) ###} +{### Rename statistics ###} +{% if data.name and data.name != o_data.name %} +ALTER STATISTICS {% if data.schema and data.schema != o_data.schema %}{{ conn|qtIdent(o_data.schema, o_data.name) }}{% else %}{{ conn|qtIdent(o_data.schema, o_data.name) }}{% endif %} + + RENAME TO {{ conn|qtIdent(data.name) }}; + +{% endif %} +{### Change schema ###} +{% if data.schema and data.schema != o_data.schema %} +ALTER STATISTICS {{ conn|qtIdent(o_data.schema, data.name if data.name else o_data.name) }} + SET SCHEMA {{ conn|qtIdent(data.schema) }}; + +{% endif %} +{### Change owner ###} +{% if data.owner and data.owner != o_data.owner %} +ALTER STATISTICS {{ conn|qtIdent(data.schema if data.schema else o_data.schema, data.name if data.name else o_data.name) }} + OWNER TO {{ conn|qtIdent(data.owner) }}; + +{% endif %} +{### Set statistics target (PostgreSQL 17+ supports DEFAULT) ###} +{% if data.stattarget is defined and data.stattarget != o_data.stattarget %} +ALTER STATISTICS {{ conn|qtIdent(data.schema if data.schema else o_data.schema, data.name if data.name else o_data.name) }} + SET STATISTICS {% if data.stattarget == -1 or data.stattarget == 'DEFAULT' %}DEFAULT{% else %}{{ data.stattarget }}{% endif %}; + +{% endif %} +{### Update comment ###} +{% if data.comment is defined and data.comment != o_data.comment %} +COMMENT ON STATISTICS {{ conn|qtIdent(data.schema if data.schema else o_data.schema, data.name if data.name else o_data.name) }} + IS {% if data.comment %}{{ data.comment|qtLiteral(conn) }}{% else %}NULL{% endif %}; +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/backend_support.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/backend_support.sql new file mode 100644 index 00000000000..4ee5299a2c3 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/backend_support.sql @@ -0,0 +1,5 @@ +{### Check if extended statistics are supported (PostgreSQL 10+) ###} +SELECT + CASE WHEN COUNT(*) > 0 THEN TRUE ELSE FALSE END AS has_statistics +FROM pg_catalog.pg_class +WHERE relname='pg_statistic_ext' diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/coll_stats.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/coll_stats.sql new file mode 100644 index 00000000000..ff796840b38 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/coll_stats.sql @@ -0,0 +1,5 @@ +{### Get collection statistics for statistics objects ###} +SELECT + COUNT(*) AS {{ conn|qtIdent(_('Statistics')) }} +FROM pg_catalog.pg_statistic_ext s +WHERE s.stxnamespace = {{scid}}::oid diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/count.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/count.sql new file mode 100644 index 00000000000..84da7fa71c8 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/count.sql @@ -0,0 +1,4 @@ +{### Count statistics objects in schema ###} +SELECT COUNT(*) +FROM pg_catalog.pg_statistic_ext s +WHERE s.stxnamespace = {{scid}}::oid diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/create.sql new file mode 100644 index 00000000000..edbe5bffdd5 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/create.sql @@ -0,0 +1,14 @@ +{### SQL to create extended statistics object (PostgreSQL 14+) ###} +{### Supports expression-based statistics ###} +CREATE STATISTICS{% if add_not_exists_clause %} IF NOT EXISTS{% endif %} {{ conn|qtIdent(data.schema, data.name) }}{% if data.stat_types and data.stat_types|length > 0 %} + + ({% for stype in data.stat_types %}{{ stype }}{% if not loop.last %}, {% endif %}{% endfor %}){% endif %} + + ON {% if data.expressions and data.expressions|length > 0 %}{% for expr in data.expressions %}({{ expr.expression if expr.expression is defined else expr }}){% if not loop.last %}, {% endif %}{% endfor %}{% elif data.columns %}{% for col in data.columns %}{{ conn|qtIdent(col) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} + + FROM {{ conn|qtIdent(data.schema, data.table) }}; +{% if data.comment %} + +COMMENT ON STATISTICS {{ conn|qtIdent(data.schema, data.name) }} + IS {{ data.comment|qtLiteral(conn) }}; +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/delete.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/delete.sql new file mode 100644 index 00000000000..05329abd2d9 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/delete.sql @@ -0,0 +1,12 @@ +{### SQL to drop extended statistics object ###} +{% if stid %} +SELECT + s.stxname AS name, + ns.nspname AS schema +FROM pg_catalog.pg_statistic_ext s + LEFT JOIN pg_catalog.pg_namespace ns ON ns.oid = s.stxnamespace +WHERE s.oid = {{stid}}::oid; +{% endif %} +{% if name %} +DROP STATISTICS{% if cascade %} CASCADE{% endif %} {{ conn|qtIdent(schema, name) }}; +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/get_oid.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/get_oid.sql new file mode 100644 index 00000000000..903336d1d8f --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/get_oid.sql @@ -0,0 +1,6 @@ +{### Get OID of newly created statistics object ###} +SELECT s.oid +FROM pg_catalog.pg_statistic_ext s + LEFT JOIN pg_catalog.pg_namespace ns ON ns.oid = s.stxnamespace +WHERE s.stxname = {{name|qtLiteral(conn)}} + AND ns.nspname = {{schema|qtLiteral(conn)}} 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 new file mode 100644 index 00000000000..07aeabeb41d --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/nodes.sql @@ -0,0 +1,17 @@ +{### List statistics objects for tree view ###} +SELECT + s.oid, + s.stxname AS name, + des.description AS comment +FROM pg_catalog.pg_statistic_ext s + LEFT OUTER JOIN pg_catalog.pg_description des + ON (des.objoid = s.oid AND des.classoid = 'pg_statistic_ext'::regclass) +WHERE s.stxnamespace = {{scid}}::oid +{% if stid %} + AND s.oid = {{stid}}::oid +{% endif %} +{% if schema_diff %} + AND CASE WHEN (SELECT COUNT(*) FROM pg_catalog.pg_depend + WHERE objid = s.oid AND deptype = 'e') > 0 THEN FALSE ELSE TRUE END +{% endif %} +ORDER BY s.stxname diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/properties.sql new file mode 100644 index 00000000000..1e38fd0d1f5 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/properties.sql @@ -0,0 +1,40 @@ +{### Query extended statistics properties from pg_statistic_ext (PostgreSQL 14+) ###} +{% if scid %} +SELECT + s.oid, + s.stxname AS name, + s.stxnamespace AS schemaoid, + ns.nspname AS schema, + s.stxrelid AS tableoid, + t.relname AS table, + pg_catalog.pg_get_userbyid(s.stxowner) AS owner, + s.stxkeys AS column_attnums, + (SELECT array_agg(a.attname ORDER BY a.attnum) + FROM pg_catalog.pg_attribute a + WHERE a.attrelid = s.stxrelid + AND a.attnum = ANY(s.stxkeys) + ) AS columns, + s.stxkind AS stat_types_raw, + CASE WHEN 'd' = ANY(s.stxkind) THEN true ELSE false END AS has_ndistinct, + CASE WHEN 'f' = ANY(s.stxkind) THEN true ELSE false END AS has_dependencies, + CASE WHEN 'm' = ANY(s.stxkind) THEN true ELSE false END AS has_mcv, + s.stxstattarget AS stattarget, +{### stxexprs added in PostgreSQL 14 for expression statistics ###} + s.stxexprs AS expressions, +{### Statistics data from pg_statistic_ext_data (PostgreSQL 12+) ###} + sd.stxdndistinct AS ndistinct_values, + sd.stxddependencies AS dependencies_values, + CASE WHEN sd.stxdmcv IS NOT NULL THEN true ELSE false END AS has_mcv_values, + des.description AS comment +FROM pg_catalog.pg_statistic_ext s + LEFT JOIN pg_catalog.pg_namespace ns ON ns.oid = s.stxnamespace + LEFT JOIN pg_catalog.pg_class t ON t.oid = s.stxrelid + LEFT JOIN pg_catalog.pg_statistic_ext_data sd ON sd.stxoid = s.oid + LEFT OUTER JOIN pg_catalog.pg_description des + ON (des.objoid = s.oid AND des.classoid = 'pg_statistic_ext'::regclass) +WHERE s.stxnamespace = {{scid}}::oid +{% if stid %} + AND s.oid = {{stid}}::oid +{% endif %} +ORDER BY s.stxname +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/stats.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/stats.sql new file mode 100644 index 00000000000..f86cc8ded51 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/stats.sql @@ -0,0 +1,30 @@ +{### Get statistics for individual statistics object (PostgreSQL 10-11) ###} +SELECT + s.stxname AS {{ conn|qtIdent(_('Name')) }}, + t.relname AS {{ conn|qtIdent(_('Table')) }}, + (SELECT string_agg(a.attname, ', ' ORDER BY a.attnum) + FROM pg_catalog.pg_attribute a + WHERE a.attrelid = s.stxrelid + AND a.attnum = ANY(s.stxkeys) + ) AS {{ conn|qtIdent(_('Columns')) }}, + CASE + WHEN s.stxkind IS NOT NULL THEN + array_to_string( + ARRAY( + SELECT CASE kind + WHEN 'd' THEN 'ndistinct' + WHEN 'f' THEN 'dependencies' + WHEN 'm' THEN 'mcv' + WHEN 'e' THEN 'expressions' + END + FROM unnest(s.stxkind) AS kind + ), ', ' + ) + ELSE '' + END AS {{ conn|qtIdent(_('Statistics Types')) }}, + NULL AS {{ conn|qtIdent(_('N-Distinct Coefficients')) }}, + NULL AS {{ conn|qtIdent(_('Functional Dependencies')) }}, + NULL AS {{ conn|qtIdent(_('Most Common Values')) }} +FROM pg_catalog.pg_statistic_ext s + LEFT JOIN pg_catalog.pg_class t ON t.oid = s.stxrelid +WHERE s.oid = {{stid}}::oid diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/update.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/update.sql new file mode 100644 index 00000000000..15965c773ed --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/update.sql @@ -0,0 +1,24 @@ +{### SQL to update extended statistics object ###} +{### Rename statistics ###} +{% if data.name and data.name != o_data.name %} +ALTER STATISTICS {{ conn|qtIdent(o_data.schema, o_data.name) }} + RENAME TO {{ conn|qtIdent(data.name) }}; + +{% endif %} +{### Change schema ###} +{% if data.schema and data.schema != o_data.schema %} +ALTER STATISTICS {{ conn|qtIdent(o_data.schema, data.name if data.name else o_data.name) }} + SET SCHEMA {{ conn|qtIdent(data.schema) }}; + +{% endif %} +{### Change owner ###} +{% if data.owner and data.owner != o_data.owner %} +ALTER STATISTICS {{ conn|qtIdent(data.schema if data.schema else o_data.schema, data.name if data.name else o_data.name) }} + OWNER TO {{ conn|qtIdent(data.owner) }}; + +{% endif %} +{### Update comment ###} +{% if data.comment is defined and data.comment != o_data.comment %} +COMMENT ON STATISTICS {{ conn|qtIdent(data.schema if data.schema else o_data.schema, data.name if data.name else o_data.name) }} + IS {% if data.comment %}{{ data.comment|qtLiteral(conn) }}{% else %}NULL{% endif %}; +{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/pg/14_plus/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/pg/14_plus/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/pg/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/pg/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/statistics_test_data.json b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/statistics_test_data.json new file mode 100644 index 00000000000..91e5f2555a7 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/statistics_test_data.json @@ -0,0 +1,250 @@ +{ + "statistics_create": [ + { + "name": "Create statistics: With ndistinct only.", + "url": "/browser/statistics/obj/", + "is_positive_test": true, + "inventory_data": { + "server_min_version": 140000, + "skip_msg": "Statistics not supported below PG 14" + }, + "test_data": { + "name": "test_stats_ndistinct", + "schema": "public", + "table": "test_table", + "columns": ["col1", "col2"], + "stat_types": ["ndistinct"] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create statistics: With dependencies only.", + "url": "/browser/statistics/obj/", + "is_positive_test": true, + "inventory_data": { + "server_min_version": 140000 + }, + "test_data": { + "name": "test_stats_dependencies", + "schema": "public", + "table": "test_table", + "columns": ["col1", "col2"], + "stat_types": ["dependencies"] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create statistics: With multiple types.", + "url": "/browser/statistics/obj/", + "is_positive_test": true, + "inventory_data": { + "server_min_version": 140000 + }, + "test_data": { + "name": "test_stats_multi", + "schema": "public", + "table": "test_table", + "columns": ["col1", "col2", "col3"], + "stat_types": ["ndistinct", "dependencies"] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create statistics: With MCV", + "url": "/browser/statistics/obj/", + "is_positive_test": true, + "inventory_data": { + "server_min_version": 140000, + "skip_msg": "Statistics node not supported below PG 14" + }, + "test_data": { + "name": "test_stats_mcv", + "schema": "public", + "table": "test_table", + "columns": ["col1", "col2"], + "stat_types": ["mcv"] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Create statistics: With invalid data - Less than 2 columns.", + "url": "/browser/statistics/obj/", + "is_positive_test": false, + "inventory_data": {}, + "test_data": { + "name": "test_stats_invalid", + "schema": "public", + "table": "test_table", + "columns": ["col1"], + "stat_types": ["ndistinct"] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 400, + "error_msg": "At least 2 columns must be specified for multi-column statistics.", + "test_result_data": {} + } + }, + { + "name": "Create statistics: With invalid data - No statistics types.", + "url": "/browser/statistics/obj/", + "is_positive_test": false, + "inventory_data": {}, + "test_data": { + "name": "test_stats_no_types", + "schema": "public", + "table": "test_table", + "columns": ["col1", "col2"], + "stat_types": [] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 400, + "error_msg": "At least 1 statistics type must be selected.", + "test_result_data": {} + } + } + ], + "statistics_delete": [ + { + "name": "Delete statistics: Single object.", + "url": "/browser/statistics/obj/", + "is_positive_test": true, + "is_list": false, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Delete statistics: Multiple objects.", + "url": "/browser/statistics/obj/", + "is_positive_test": true, + "is_list": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Delete statistics: Non-existing object.", + "url": "/browser/statistics/obj/", + "is_positive_test": false, + "is_list": false, + "inventory_data": {}, + "test_data": { + "statistics_id": 999999 + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 410, + "error_msg": "could not find the specified statistics.", + "test_result_data": {} + } + } + ], + "statistics_get": [ + { + "name": "Get statistics: Single object properties.", + "url": "/browser/statistics/obj/", + "is_positive_test": true, + "is_list": false, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Get statistics: List all objects.", + "url": "/browser/statistics/obj/", + "is_positive_test": true, + "is_list": true, + "inventory_data": {}, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, + { + "name": "Get statistics: Non-existing object.", + "url": "/browser/statistics/obj/", + "is_positive_test": false, + "is_list": false, + "inventory_data": {}, + "test_data": { + "statistics_id": 999999 + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 410, + "error_msg": "could not find the specified statistics.", + "test_result_data": {} + } + } + ], + "statistics_put": [ + { + "name": "Update statistics: Change name.", + "url": "/browser/statistics/obj/", + "is_positive_test": true, + "inventory_data": {}, + "test_data": { + "name": "test_stats_renamed" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + } + ] +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_add.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_add.py new file mode 100644 index 00000000000..511cb9430b8 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_add.py @@ -0,0 +1,121 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2025, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +from unittest.mock import patch + +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils +from pgadmin.utils import server_utils +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from . import utils as statistics_utils + + +class StatisticsAddTestCase(BaseTestGenerator): + """This class will add new statistics object under schema node.""" + + # Generates scenarios + scenarios = utils.generate_scenarios("statistics_create", + statistics_utils.test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + # Create db connection + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception("Could not connect to database to add statistics.") + + # Check server version (Statistics require PG 14+) + if "server_min_version" in self.inventory_data: + server_con = server_utils.connect_server(self, self.server_id) + if server_con["info"] != "Server connected.": + raise Exception("Could not connect to server to check version") + if server_con["data"]["version"] < \ + self.inventory_data["server_min_version"]: + self.skipTest(self.inventory_data.get("skip_msg", + "Statistics not supported below PG 14")) + + # Create schema + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to add statistics.") + + # Create test table for statistics + self.table_name = "test_table_stats_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = statistics_utils.create_table_for_statistics( + self.server, + self.db_name, + self.schema_name, + self.table_name + ) + + def runTest(self): + """This function will add statistics under schema node.""" + db_user = self.server["username"] + self.data["schema"] = self.schema_name + self.data["table"] = self.table_name + + if "name" in self.data: + statistics_name = \ + self.data["name"] + (str(uuid.uuid4())[1:8]) + self.data["name"] = statistics_name + + if self.is_positive_test: + response = statistics_utils.api_create(self) + + # Assert response + utils.assert_status_code(self, response) + + # Verify in backend + if "name" in self.data: + cross_check_res = statistics_utils.verify_statistics( + self.server, + self.db_name, + self.data["name"] + ) + + self.assertIsNotNone( + cross_check_res, + "Could not find the newly created statistics object." + ) + else: + if self.mocking_required: + with patch(self.mock_data["function_name"], + side_effect=eval(self.mock_data["return_value"])): + response = statistics_utils.api_create(self) + + # Assert response + utils.assert_status_code(self, response) + utils.assert_error_message(self, response) + else: + response = statistics_utils.api_create(self) + + # Assert response + utils.assert_status_code(self, response) + utils.assert_error_message(self, response) + + def tearDown(self): + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_delete.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_delete.py new file mode 100644 index 00000000000..dff34617216 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_delete.py @@ -0,0 +1,159 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2025, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +from unittest.mock import patch + +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils +from pgadmin.utils import server_utils +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from . import utils as statistics_utils + + +class StatisticsDeleteTestCase(BaseTestGenerator): + """This class will delete the statistics object under schema node.""" + + # Generates scenarios + scenarios = utils.generate_scenarios("statistics_delete", + statistics_utils.test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + # Create db connection + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception("Could not connect to database to delete " + "statistics.") + + # Check server version (Statistics require PG 14+) + server_con = server_utils.connect_server(self, self.server_id) + if server_con["info"] != "Server connected.": + raise Exception("Could not connect to server to check version") + if server_con["data"]["version"] < 140000: + self.skipTest("Statistics not supported below PG 14") + + # Create schema + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to delete statistics") + + # Create test table for statistics + self.table_name = "test_table_stats_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = statistics_utils.create_table_for_statistics( + self.server, + self.db_name, + self.schema_name, + self.table_name + ) + + # Create statistics object + self.statistics_name = "test_stats_delete_%s" % \ + (str(uuid.uuid4())[1:8]) + + self.statistics_id = statistics_utils.create_statistics( + self.server, + self.db_name, + self.schema_name, + self.table_name, + self.statistics_name, + ["col1", "col2"], + ["ndistinct", "dependencies"] + ) + + statistics_response = statistics_utils.verify_statistics( + self.server, self.db_name, self.statistics_name + ) + if not statistics_response: + raise Exception("Could not find the statistics to delete.") + + # For multiple delete test + if self.is_list: + self.statistics_name_2 = "test_stats_delete_%s" % \ + (str(uuid.uuid4())[1:8]) + + self.statistics_id_2 = statistics_utils.create_statistics( + self.server, + self.db_name, + self.schema_name, + self.table_name, + self.statistics_name_2, + ["col1", "col2"], + ["ndistinct"] + ) + + statistics_response = statistics_utils.verify_statistics( + self.server, self.db_name, self.statistics_name_2 + ) + if not statistics_response: + raise Exception("Could not find the second statistics " + "to delete.") + + # List to delete multiple statistics + self.data['ids'] = [self.statistics_id, self.statistics_id_2] + + def runTest(self): + """This function will delete the statistics under schema node.""" + + if self.is_positive_test: + if self.is_list: + response = statistics_utils.api_delete(self, '') + else: + response = statistics_utils.api_delete(self) + + # Assert response + utils.assert_status_code(self, response) + + # Verify in backend + statistics_response = statistics_utils.verify_statistics( + self.server, self.db_name, self.statistics_name + ) + self.assertIsNone(statistics_response, + "Deleted statistics still present") + + if self.is_list: + statistics_response_2 = statistics_utils.verify_statistics( + self.server, self.db_name, self.statistics_name_2 + ) + self.assertIsNone(statistics_response_2, + "Second deleted statistics still present") + else: + if self.mocking_required: + with patch( + self.mock_data["function_name"], + side_effect=[eval(self.mock_data["return_value"])] + ): + response = statistics_utils.api_delete(self) + elif 'statistics_id' in self.data: + self.statistics_id = self.data["statistics_id"] + response = statistics_utils.api_delete(self) + + # Assert response + utils.assert_status_code(self, response) + utils.assert_error_message(self, response) + + def tearDown(self): + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_delete_multiple.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_delete_multiple.py new file mode 100644 index 00000000000..5edbb35f328 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_delete_multiple.py @@ -0,0 +1,125 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2025, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import json + +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils +from pgadmin.utils import server_utils +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from . import utils as statistics_utils + + +class StatisticsDeleteMultipleTestCase(BaseTestGenerator): + """This class will delete multiple statistics objects under schema node""" + + scenarios = [ + # Fetching default URL for statistics node. + ('Fetch statistics Node URL', dict(url='/browser/statistics/obj/')) + ] + + def setUp(self): + super().setUp() + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception("Could not connect to database to add statistics") + + # Check server version (Statistics require PG 14+) + server_con = server_utils.connect_server(self, self.server_id) + if server_con["info"] != "Server connected.": + raise Exception("Could not connect to server to check version") + if server_con["data"]["version"] < 140000: + self.skipTest("Statistics not supported below PG 14") + + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to add statistics.") + + # Create test table for statistics + self.table_name = "test_table_stats_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = statistics_utils.create_table_for_statistics( + self.server, + self.db_name, + self.schema_name, + self.table_name + ) + + # Create multiple statistics objects + self.statistics_name = "test_stats_delete_%s" % str(uuid.uuid4())[1:8] + self.statistics_name_1 = "test_stats_delete_%s" % \ + str(uuid.uuid4())[1:8] + + self.statistics_ids = [ + statistics_utils.create_statistics( + self.server, + self.db_name, + self.schema_name, + self.table_name, + self.statistics_name, + ["col1", "col2"], + ["ndistinct", "dependencies"] + ), + statistics_utils.create_statistics( + self.server, + self.db_name, + self.schema_name, + self.table_name, + self.statistics_name_1, + ["col1", "col2"], + ["mcv"] + ) + ] + + def runTest(self): + """This function will delete multiple statistics under schema node""" + statistics_response = statistics_utils.verify_statistics( + self.server, + self.db_name, + self.statistics_name + ) + if not statistics_response: + raise Exception("Could not find the statistics to delete.") + + statistics_response = statistics_utils.verify_statistics( + self.server, + self.db_name, + self.statistics_name_1 + ) + if not statistics_response: + raise Exception("Could not find the statistics to delete.") + + data = json.dumps({'ids': self.statistics_ids}) + response = self.tester.delete( + self.url + str(utils.SERVER_GROUP) + '/' + + str(self.server_id) + '/' + + str(self.db_id) + '/' + + str(self.schema_id) + '/', + follow_redirects=True, + data=data, + content_type='html/json' + ) + self.assertEqual(response.status_code, 200) + + def tearDown(self): + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_get.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_get.py new file mode 100644 index 00000000000..6722929c007 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_get.py @@ -0,0 +1,150 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2025, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import uuid +import json +from unittest.mock import patch + +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils +from pgadmin.utils import server_utils +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from . import utils as statistics_utils + + +class StatisticsGetTestCase(BaseTestGenerator): + """This class will fetch the statistics under schema node.""" + + # Generates scenarios + scenarios = utils.generate_scenarios("statistics_get", + statistics_utils.test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + # Create db connection + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception("Could not connect to database to fetch " + "statistics.") + + # Check server version (Statistics require PG 14+) + if "server_min_version" in self.data: + server_con = server_utils.connect_server(self, self.server_id) + if server_con["info"] != "Server connected.": + raise Exception( + "Could not connect to server to check version") + ver = server_con["data"]["version"] + if ver < self.data["server_min_version"]: + self.skipTest(self.data["skip_msg"]) + + if "server_max_version" in self.data: + server_con = server_utils.connect_server(self, self.server_id) + if server_con["info"] != "Server connected.": + raise Exception("Could not connect to server to check version") + ver = server_con["data"]["version"] + if ver > self.data["server_max_version"]: + self.skipTest(self.data["skip_msg"]) + + # Create schema + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to fetch statistics") + + # Create test table for statistics + self.table_name = "test_table_stats_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = statistics_utils.create_table_for_statistics( + self.server, + self.db_name, + self.schema_name, + self.table_name + ) + + # Create statistics object + self.statistics_name = "test_stats_get_%s" % (str(uuid.uuid4())[1:8]) + + self.statistics_id = statistics_utils.create_statistics( + self.server, + self.db_name, + self.schema_name, + self.table_name, + self.statistics_name, + ["col1", "col2"], + ["ndistinct", "dependencies"] + ) + + # In case of multiple statistics + if self.is_list: + self.statistics_name_2 = "test_stats_get_%s" % \ + (str(uuid.uuid4())[1:8]) + self.statistics_id_2 = statistics_utils.create_statistics( + self.server, + self.db_name, + self.schema_name, + self.table_name, + self.statistics_name_2, + ["col1", "col2"], + ["mcv"] + ) + + def runTest(self): + """This function will fetch the statistics under schema node.""" + if self.is_positive_test: + if self.is_list: + response = statistics_utils.api_get(self, '') + else: + response = statistics_utils.api_get(self) + + # Assert response + utils.assert_status_code(self, response) + + # Check response data + response_data = json.loads(response.data.decode('utf-8')) + if not self.is_list: + # Verify that the statistics name is in the response + self.assertIn('name', response_data) + self.assertEqual(response_data['name'], self.statistics_name) + # Verify columns are returned + self.assertIn('columns', response_data) + # Verify stat_types are returned + self.assertIn('stat_types', response_data) + else: + if self.mocking_required: + with patch( + self.mock_data["function_name"], + side_effect=[eval(self.mock_data["return_value"])] + ): + response = statistics_utils.api_get(self) + elif 'statistics_id' in self.data: + # Non-existing statistics id + self.statistics_id = self.data["statistics_id"] + response = statistics_utils.api_get(self) + + # Assert response + utils.assert_status_code(self, response) + utils.assert_error_message(self, response) + + def tearDown(self): + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_put.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_put.py new file mode 100644 index 00000000000..c44d1ac529e --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_put.py @@ -0,0 +1,126 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2025, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json +import uuid +from unittest.mock import patch + +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils +from pgadmin.utils import server_utils +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from . import utils as statistics_utils + + +class StatisticsUpdateTestCase(BaseTestGenerator): + """This class will update the statistics under schema node.""" + + # Generates scenarios + scenarios = utils.generate_scenarios("statistics_put", + statistics_utils.test_cases) + + def setUp(self): + super().setUp() + # Load test data + self.data = self.test_data + + # Create db connection + self.db_name = parent_node_dict["database"][-1]["db_name"] + schema_info = parent_node_dict["schema"][-1] + self.server_id = schema_info["server_id"] + self.db_id = schema_info["db_id"] + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.server_id, self.db_id) + if not db_con['data']["connected"]: + raise Exception("Could not connect to database to update " + "statistics.") + + # Check server version (Statistics require PG 14+) + server_con = server_utils.connect_server(self, self.server_id) + if server_con["info"] != "Server connected.": + raise Exception("Could not connect to server to check version") + if server_con["data"]["version"] < 140000: + self.skipTest("Statistics not supported below PG 14") + + # Create schema + self.schema_id = schema_info["schema_id"] + self.schema_name = schema_info["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise Exception("Could not find the schema to update statistics") + + # Create test table for statistics + self.table_name = "test_table_stats_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = statistics_utils.create_table_for_statistics( + self.server, + self.db_name, + self.schema_name, + self.table_name + ) + + # Create statistics object + self.statistics_name = "test_stats_update_%s" % \ + (str(uuid.uuid4())[1:8]) + + self.statistics_id = statistics_utils.get_statistics_id( + self.server, self.db_name, self.statistics_name + ) + if self.statistics_id is None: + self.statistics_id = statistics_utils.create_statistics( + self.server, + self.db_name, + self.schema_name, + self.table_name, + self.statistics_name, + ["col1", "col2"], + ["ndistinct", "dependencies"] + ) + + def runTest(self): + """This function will update the statistics under schema node.""" + statistics_response = statistics_utils.verify_statistics( + self.server, self.db_name, self.statistics_name + ) + if not statistics_response: + raise Exception("Could not find the statistics to update.") + + self.data["oid"] = self.statistics_id + + if self.is_positive_test: + response = statistics_utils.api_put(self) + + # Assert response + utils.assert_status_code(self, response) + + # Verify the update in the response + response_data = json.loads(response.data.decode('utf-8')) + self.assertIn('node', response_data) + else: + if self.mocking_required: + with patch( + self.mock_data["function_name"], + side_effect=[eval(self.mock_data["return_value"])] + ): + response = statistics_utils.api_put(self) + else: + response = statistics_utils.api_put(self) + + # Assert response + utils.assert_status_code(self, response) + utils.assert_error_message(self, response) + + def tearDown(self): + # Disconnect the database + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/utils.py new file mode 100644 index 00000000000..2cc70bb0868 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/utils.py @@ -0,0 +1,332 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2025, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Utility functions for Statistics tests""" + +import sys +import traceback +import os +import json + +from regression.python_test_utils import test_utils +from urllib.parse import urlencode + +# Load test data from JSON file +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/statistics_test_data.json") as data_file: + test_cases = json.load(data_file) + + +# API call methods +def api_create(self): + """Create a new statistics object via API""" + return self.tester.post("{0}{1}/{2}/{3}/{4}/". + format(self.url, test_utils.SERVER_GROUP, + self.server_id, self.db_id, + self.schema_id), + data=json.dumps(self.data), + content_type='html/json') + + +def api_get(self, stid=None): + """Get statistics object details via API""" + if stid is None: + stid = self.statistics_id + return self.tester.get("{0}{1}/{2}/{3}/{4}/{5}". + format(self.url, test_utils.SERVER_GROUP, + self.server_id, self.db_id, + self.schema_id, stid), + follow_redirects=True) + + +def api_delete(self, stid=None): + """Delete a statistics object via API""" + if stid is None: + stid = self.statistics_id + return self.tester.delete("{0}{1}/{2}/{3}/{4}/{5}". + format(self.url, test_utils.SERVER_GROUP, + self.server_id, self.db_id, + self.schema_id, stid), + data=json.dumps(self.data), + follow_redirects=True) + + +def api_put(self): + """Update a statistics object via API""" + return self.tester.put("{0}{1}/{2}/{3}/{4}/{5}". + format(self.url, test_utils.SERVER_GROUP, + self.server_id, self.db_id, + self.schema_id, self.statistics_id), + data=json.dumps(self.data), + follow_redirects=True) + + +def api_get_msql(self, url_encode_data): + """Get modified SQL via API""" + return self.tester.get("{0}{1}/{2}/{3}/{4}/{5}?{6}". + format(self.url, test_utils.SERVER_GROUP, + self.server_id, self.db_id, + self.schema_id, self.statistics_id, + urlencode(url_encode_data)), + follow_redirects=True) + + +def create_table_for_statistics(server, db_name, schema_name, table_name): + """ + This function creates a table with multiple columns for statistics testing. + + Args: + server: server details + db_name: database name + schema_name: schema name + table_name: table name + + Returns: + table OID + """ + try: + connection = test_utils.get_db_connection( + db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode'] + ) + old_isolation_level = connection.isolation_level + test_utils.set_isolation_level(connection, 0) + pg_cursor = connection.cursor() + + # Create a table with multiple columns for statistics testing + query = f"CREATE TABLE {schema_name}.{table_name} " \ + f"(col1 INTEGER, col2 INTEGER, col3 TEXT)" + pg_cursor.execute(query) + + # Insert some test data + insert_query = f"INSERT INTO {schema_name}.{table_name} VALUES " \ + f"(1, 10, 'test1'), (2, 20, 'test2'), (3, 30, 'test3')" + pg_cursor.execute(insert_query) + + test_utils.set_isolation_level(connection, old_isolation_level) + connection.commit() + + # Get OID + pg_cursor.execute( + f"SELECT oid FROM pg_catalog.pg_class " + f"WHERE relname = '{table_name}' " + f"AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace " + f"WHERE nspname = '{schema_name}')" + ) + table = pg_cursor.fetchone() + table_oid = table[0] if table else None + connection.close() + + return table_oid + except Exception: + traceback.print_exc(file=sys.stderr) + raise + + +def create_statistics(server, db_name, schema_name, table_name, + statistics_name, columns, stat_types): + """ + This function creates a statistics object in the database. + + Args: + server: server details + db_name: database name + schema_name: schema name + table_name: table name + statistics_name: statistics object name + columns: list of column names + stat_types: list of statistics types (ndistinct, dependencies, mcv) + + Returns: + statistics OID + """ + try: + connection = test_utils.get_db_connection( + db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode'] + ) + old_isolation_level = connection.isolation_level + test_utils.set_isolation_level(connection, 0) + pg_cursor = connection.cursor() + + # Create statistics + stat_types_str = ', '.join(stat_types) + columns_str = ', '.join(columns) + query = f"CREATE STATISTICS {schema_name}.{statistics_name} " \ + f"({stat_types_str}) ON {columns_str} " \ + f"FROM {schema_name}.{table_name}" + pg_cursor.execute(query) + + # Get OID + pg_cursor.execute( + f"SELECT s.oid FROM pg_catalog.pg_statistic_ext s " + f"JOIN pg_catalog.pg_namespace n ON s.stxnamespace = n.oid " + f"WHERE s.stxname = '{statistics_name}' " + f"AND n.nspname = '{schema_name}'" + ) + statistics = pg_cursor.fetchone() + statistics_oid = statistics[0] + test_utils.set_isolation_level(connection, old_isolation_level) + connection.commit() + connection.close() + + return statistics_oid + except Exception: + traceback.print_exc(file=sys.stderr) + raise + + +def verify_statistics(server, db_name, statistics_name): + """ + This function verifies that a statistics object exists in the database. + + Args: + server: server details + db_name: database name + statistics_name: statistics object name + + Returns: + statistics details (oid, name) + """ + try: + connection = test_utils.get_db_connection( + db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode'] + ) + pg_cursor = connection.cursor() + pg_cursor.execute( + f"SELECT s.oid, s.stxname FROM pg_catalog.pg_statistic_ext s " + f"WHERE s.stxname = '{statistics_name}'" + ) + statistics = pg_cursor.fetchone() + connection.close() + return statistics + except Exception: + traceback.print_exc(file=sys.stderr) + raise + + +def get_statistics_id(server, db_name, statistics_name): + """ + This function retrieves the OID of a statistics object. + + Args: + server: server details + db_name: database name + statistics_name: statistics object name + + Returns: + statistics OID or None + """ + try: + connection = test_utils.get_db_connection( + db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode'] + ) + pg_cursor = connection.cursor() + pg_cursor.execute( + f"SELECT s.oid FROM pg_catalog.pg_statistic_ext s " + f"WHERE s.stxname = '{statistics_name}'" + ) + statistics = pg_cursor.fetchone() + statistics_id = statistics[0] if statistics else None + connection.close() + return statistics_id + except Exception: + traceback.print_exc(file=sys.stderr) + raise + + +def delete_statistics(server, db_name, schema_name, statistics_name): + """ + This function deletes a statistics object from the database. + + Args: + server: server details + db_name: database name + schema_name: schema name + statistics_name: statistics object name + + Returns: + None + """ + try: + connection = test_utils.get_db_connection( + db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode'] + ) + old_isolation_level = connection.isolation_level + test_utils.set_isolation_level(connection, 0) + pg_cursor = connection.cursor() + + query = f"DROP STATISTICS IF EXISTS {schema_name}.{statistics_name}" + pg_cursor.execute(query) + + test_utils.set_isolation_level(connection, old_isolation_level) + connection.commit() + connection.close() + except Exception: + traceback.print_exc(file=sys.stderr) + raise + + +def get_statistics_columns(server, db_name, statistics_oid): + """ + This function retrieves the columns for a statistics object. + + Args: + server: server details + db_name: database name + statistics_oid: statistics object OID + + Returns: + list of column names + """ + try: + connection = test_utils.get_db_connection( + db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode'] + ) + pg_cursor = connection.cursor() + pg_cursor.execute( + f"SELECT array_agg(a.attname) FROM pg_catalog.pg_attribute a " + f"JOIN pg_catalog.pg_statistic_ext s ON a.attrelid = s.stxrelid " + f"WHERE s.oid = {statistics_oid} " + f"AND a.attnum = ANY(s.stxkeys)" + ) + columns = pg_cursor.fetchone() + connection.close() + return columns[0] if columns else [] + except Exception: + traceback.print_exc(file=sys.stderr) + raise diff --git a/web/webpack.config.js b/web/webpack.config.js index d05adcd8beb..020889f33a0 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -213,6 +213,7 @@ module.exports = [{ 'pure|pgadmin.node.trigger_function', 'pure|pgadmin.node.package', 'pure|pgadmin.node.sequence', + 'pure|pgadmin.node.statistics', 'pure|pgadmin.node.synonym', 'pure|pgadmin.node.type', 'pure|pgadmin.node.rule', diff --git a/web/webpack.shim.js b/web/webpack.shim.js index c370b31af72..82081b34e09 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -129,6 +129,7 @@ let webpackShimConfig = { 'pgadmin.node.schema': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/schema'), 'pgadmin.node.schema.dir': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/static/js/'), 'pgadmin.node.sequence': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/sequences/static/js/sequence'), + 'pgadmin.node.statistics': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/js/statistics'), 'pgadmin.node.server': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/server'), 'pgadmin.node.server_group': path.join(__dirname, './pgadmin/browser/server_groups/static/js/server_group'), 'pgadmin.node.synonym': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/synonyms/static/js/synonym'), From 382cffe5d37c6f53024cf79d75bf44cbf852eb06 Mon Sep 17 00:00:00 2001 From: Murtuza Zabuawala Date: Wed, 10 Jun 2026 10:02:14 +0530 Subject: [PATCH 2/2] chore: fixed additional review comments and added docs for the node --- docs/en_US/images/statistics_definition.png | Bin 0 -> 49224 bytes docs/en_US/images/statistics_general.png | Bin 0 -> 89250 bytes docs/en_US/images/statistics_sql.png | Bin 0 -> 52318 bytes docs/en_US/managing_database_objects.rst | 1 + docs/en_US/release_notes_9_16.rst | 1 + docs/en_US/statistics_dialog.rst | 75 ++++++++++++++++++ .../databases/schemas/statistics/__init__.py | 9 +-- .../statistics/static/js/statistics.ui.js | 4 +- .../statistics/sql/16_plus/create.sql | 2 +- .../statistics/sql/16_plus/properties.sql | 2 +- .../statistics/sql/16_plus/update.sql | 3 +- .../statistics/sql/17_plus/create.sql | 2 +- .../statistics/sql/17_plus/properties.sql | 2 +- .../statistics/sql/17_plus/update.sql | 3 +- .../statistics/sql/default/create.sql | 2 +- .../statistics/sql/default/properties.sql | 2 +- .../tests/statistics_test_data.json | 23 ++++++ .../tests/test_statistics_delete.py | 15 +++- .../statistics/tests/test_statistics_get.py | 17 ++-- .../schemas/statistics/tests/utils.py | 71 ++++++++++++++++- 20 files changed, 204 insertions(+), 30 deletions(-) create mode 100644 docs/en_US/images/statistics_definition.png create mode 100644 docs/en_US/images/statistics_general.png create mode 100644 docs/en_US/images/statistics_sql.png create mode 100644 docs/en_US/statistics_dialog.rst diff --git a/docs/en_US/images/statistics_definition.png b/docs/en_US/images/statistics_definition.png new file mode 100644 index 0000000000000000000000000000000000000000..0b4e0692f11330246549f1ec6a288f5b9a365b6d GIT binary patch literal 49224 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%0DBz3ls-hVvc;H84h%`_$ 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 literal 0 HcmV?d00001 diff --git a/docs/en_US/images/statistics_general.png b/docs/en_US/images/statistics_general.png new file mode 100644 index 0000000000000000000000000000000000000000..6aa2906e201180cdfa34092cab7ead168dcbf18a GIT binary patch literal 89250 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 ZUXZ*DtHf~=>|OzX;vzD_Wr8~1{|A}SbdUf5 literal 0 HcmV?d00001 diff --git a/docs/en_US/images/statistics_sql.png b/docs/en_US/images/statistics_sql.png new file mode 100644 index 0000000000000000000000000000000000000000..a78ab4df9f6a32eb7b7dbdcd20f9e33252c03f7f GIT binary patch literal 52318 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 literal 0 HcmV?d00001 diff --git a/docs/en_US/managing_database_objects.rst b/docs/en_US/managing_database_objects.rst index 5b297568555..a55d6d72d17 100644 --- a/docs/en_US/managing_database_objects.rst +++ b/docs/en_US/managing_database_objects.rst @@ -39,6 +39,7 @@ node, and select *Create Cast...* publication_dialog schema_dialog sequence_dialog + statistics_dialog subscription_dialog synonym_dialog trigger_function_dialog diff --git a/docs/en_US/release_notes_9_16.rst b/docs/en_US/release_notes_9_16.rst index 4f0740cdff0..406c1277a90 100644 --- a/docs/en_US/release_notes_9_16.rst +++ b/docs/en_US/release_notes_9_16.rst @@ -29,6 +29,7 @@ New features | `Issue #9626 `_ - Add support for the TOAST tuple target storage parameter in the Materialized View dialog. | `Issue #9646 `_ - Make the init container security context in the Helm chart configurable via containerSecurityContext, consistent with the main container. | `Issue #9699 `_ - Add support for closing a tab with a middle-click on its title. + | `Issue #2018 `_ - Added support for Extended Statistics objects (CREATE STATISTICS) under the Schema node, with full create, edit, delete, and SQL-generation support for column-based and expression-based statistics on PostgreSQL 14+. Housekeeping ************ diff --git a/docs/en_US/statistics_dialog.rst b/docs/en_US/statistics_dialog.rst new file mode 100644 index 00000000000..ef3094f4119 --- /dev/null +++ b/docs/en_US/statistics_dialog.rst @@ -0,0 +1,75 @@ +.. _statistics_dialog: + +************************** +`Statistics Dialog`:index: +************************** + +Use the *Statistics* dialog to define extended statistics on one or more columns +(or expressions) of a table. Extended statistics let PostgreSQL collect +correlation data across columns that can significantly improve query-plan +estimates for queries that filter or group by multiple columns. + +Extended statistics require PostgreSQL 14 or later. + +The *Statistics* dialog organizes options across the *General* and *Definition* +tabs. The *SQL* tab displays the SQL command generated by your selections. + +.. image:: images/statistics_general.png + :alt: Statistics dialog general tab + :align: center + +Use the fields in the *General* tab to identify the statistics object: + +* Use the *Name* field to enter a descriptive name. On PostgreSQL 16 and later, + the name is optional — PostgreSQL will auto-generate one if left blank. +* Use the *Owner* field to select the role that will own the statistics object. +* Use the *Schema* field to select the schema in which the statistics object + will reside. +* Use the *Comment* field to store an optional note about the statistics object. + +Click the *Definition* tab to continue. + +.. image:: images/statistics_definition.png + :alt: Statistics dialog definition tab + :align: center + +Use the fields in the *Definition* tab to describe the statistics: + +* Use the *Table* field to select the table on which the statistics will be + collected. The list is filtered to tables in the selected schema. +* Use the *Columns* field to select two or more columns. Hold *Ctrl* (or *Cmd* + on macOS) to select multiple columns. At least two columns are required when + collecting column-based statistics. +* Use the *Expressions* field to enter one or more SQL expressions separated by + commas (e.g. ``lower(col1), (col1 + col2)``). Specify expressions instead of + columns when you want statistics on derived values. +* Use the *Statistics types* field to choose which kinds of extended statistics + to collect: + + * *N-distinct* — estimates the number of distinct value combinations across + the selected columns or expressions. + * *Dependencies* — detects functional dependencies between columns, improving + estimates for queries with correlated ``WHERE`` clauses. + * *MCV (Most Common Values)* — records the most common combinations of values, + available on PostgreSQL 12 and later. + +Click the *SQL* tab to continue. + + +Your entries in the *Statistics* dialog generate a SQL command (see an example +below). Use the *SQL* tab for review; revisit or switch tabs to make any changes. + +Example +******* + +The following is an example of the SQL command generated by user selections in +the *Statistics* dialog: + +.. image:: images/statistics_sql.png + :alt: Statistics dialog SQL tab + :align: center + +* Click the *Info* button (i) to access online help. +* Click the *Save* button to save work. +* Click the *Close* button to exit without saving work. +* Click the *Reset* button to restore configuration parameters. diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/__init__.py index d0428b39476..844f4dc56df 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/__init__.py @@ -363,12 +363,9 @@ def _fetch_properties(self, scid, stid): if row.get('columns') is None: row['columns'] = [] - # Set has_expressions boolean based on expressions field - # expressions is pg_node_tree (bytea) - we just check if it exists - row['has_expressions'] = row.get('expressions') is not None - - # Remove expressions from display (it's internal PostgreSQL format) - row.pop('expressions', None) + # expression_list is the deparsed text from pg_get_expr; keep it for + # SQL-tab round-trip and set has_expressions for the properties switch + row['has_expressions'] = row.get('expression_list') is not None # Ensure stattarget has a default value if None if row.get('stattarget') is None: diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/js/statistics.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/js/statistics.ui.js index 7457476ac41..16ddb63a13b 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/js/statistics.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/static/js/statistics.ui.js @@ -116,7 +116,9 @@ export default class StatisticsSchema extends BaseUISchema { let tid = obj.getTableOid(state.table); return { type: 'select', - options: state.table ? ()=>obj.fieldOptions.getColumns({tid: tid}) : [], + options: (state.table && tid) + ? () => obj.fieldOptions.getColumns({tid: tid}) + : (state.columns || []).map(col => ({label: col, value: col})), optionsReloadBasis: state.table, }; }, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/create.sql index d1c356f53f5..7a248f26d38 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/create.sql @@ -4,7 +4,7 @@ CREATE STATISTICS{% if data.name %}{% if add_not_exists_clause %} IF NOT EXISTS{ ({% for stype in data.stat_types %}{{ stype }}{% if not loop.last %}, {% endif %}{% endfor %}){% endif %} - ON {% if data.expressions and data.expressions|length > 0 %}{% for expr in data.expressions %}({{ expr.expression if expr.expression is defined else expr }}){% if not loop.last %}, {% endif %}{% endfor %}{% elif data.columns %}{% for col in data.columns %}{{ conn|qtIdent(col) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} + ON {% if data.expressions and data.expressions|length > 0 %}{% for expr in data.expressions %}({{ expr.expression if expr.expression is defined else expr }}){% if not loop.last %}, {% endif %}{% endfor %}{% elif data.expression_list %}{{ data.expression_list }}{% elif data.columns %}{% for col in data.columns %}{{ conn|qtIdent(col) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} FROM {{ conn|qtIdent(data.schema, data.table) }}; {% if data.comment and data.name %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/properties.sql index 1e38fd0d1f5..1663a0a757d 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/properties.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/properties.sql @@ -20,7 +20,7 @@ SELECT CASE WHEN 'm' = ANY(s.stxkind) THEN true ELSE false END AS has_mcv, s.stxstattarget AS stattarget, {### stxexprs added in PostgreSQL 14 for expression statistics ###} - s.stxexprs AS expressions, + pg_catalog.pg_get_expr(s.stxexprs, s.stxrelid) AS expression_list, {### Statistics data from pg_statistic_ext_data (PostgreSQL 12+) ###} sd.stxdndistinct AS ndistinct_values, sd.stxddependencies AS dependencies_values, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/update.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/update.sql index 2e528fc01dd..1e9b79e44bc 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/update.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/16_plus/update.sql @@ -1,8 +1,7 @@ {### SQL to update extended statistics object (PostgreSQL 16+) ###} {### Rename statistics ###} {% if data.name and data.name != o_data.name %} -ALTER STATISTICS {% if data.schema and data.schema != o_data.schema %}{{ conn|qtIdent(o_data.schema, o_data.name) }}{% else %}{{ conn|qtIdent(o_data.schema, o_data.name) }}{% endif %} - +ALTER STATISTICS {{ conn|qtIdent(o_data.schema, o_data.name) }} RENAME TO {{ conn|qtIdent(data.name) }}; {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/create.sql index b8102738f57..500d4982c8e 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/create.sql @@ -4,7 +4,7 @@ CREATE STATISTICS{% if data.name %}{% if add_not_exists_clause %} IF NOT EXISTS{ ({% for stype in data.stat_types %}{{ stype }}{% if not loop.last %}, {% endif %}{% endfor %}){% endif %} - ON {% if data.expressions and data.expressions|length > 0 %}{% for expr in data.expressions %}({{ expr.expression if expr.expression is defined else expr }}){% if not loop.last %}, {% endif %}{% endfor %}{% elif data.columns %}{% for col in data.columns %}{{ conn|qtIdent(col) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} + ON {% if data.expressions and data.expressions|length > 0 %}{% for expr in data.expressions %}({{ expr.expression if expr.expression is defined else expr }}){% if not loop.last %}, {% endif %}{% endfor %}{% elif data.expression_list %}{{ data.expression_list }}{% elif data.columns %}{% for col in data.columns %}{{ conn|qtIdent(col) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} FROM {{ conn|qtIdent(data.schema, data.table) }}; {% if data.comment and data.name %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/properties.sql index 1e38fd0d1f5..1663a0a757d 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/properties.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/properties.sql @@ -20,7 +20,7 @@ SELECT CASE WHEN 'm' = ANY(s.stxkind) THEN true ELSE false END AS has_mcv, s.stxstattarget AS stattarget, {### stxexprs added in PostgreSQL 14 for expression statistics ###} - s.stxexprs AS expressions, + pg_catalog.pg_get_expr(s.stxexprs, s.stxrelid) AS expression_list, {### Statistics data from pg_statistic_ext_data (PostgreSQL 12+) ###} sd.stxdndistinct AS ndistinct_values, sd.stxddependencies AS dependencies_values, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/update.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/update.sql index 830ea36f321..802aa59f93e 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/update.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/17_plus/update.sql @@ -1,8 +1,7 @@ {### SQL to update extended statistics object (PostgreSQL 17+) ###} {### Rename statistics ###} {% if data.name and data.name != o_data.name %} -ALTER STATISTICS {% if data.schema and data.schema != o_data.schema %}{{ conn|qtIdent(o_data.schema, o_data.name) }}{% else %}{{ conn|qtIdent(o_data.schema, o_data.name) }}{% endif %} - +ALTER STATISTICS {{ conn|qtIdent(o_data.schema, o_data.name) }} RENAME TO {{ conn|qtIdent(data.name) }}; {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/create.sql index edbe5bffdd5..e54d25b59d8 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/create.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/create.sql @@ -4,7 +4,7 @@ CREATE STATISTICS{% if add_not_exists_clause %} IF NOT EXISTS{% endif %} {{ conn ({% for stype in data.stat_types %}{{ stype }}{% if not loop.last %}, {% endif %}{% endfor %}){% endif %} - ON {% if data.expressions and data.expressions|length > 0 %}{% for expr in data.expressions %}({{ expr.expression if expr.expression is defined else expr }}){% if not loop.last %}, {% endif %}{% endfor %}{% elif data.columns %}{% for col in data.columns %}{{ conn|qtIdent(col) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} + ON {% if data.expressions and data.expressions|length > 0 %}{% for expr in data.expressions %}({{ expr.expression if expr.expression is defined else expr }}){% if not loop.last %}, {% endif %}{% endfor %}{% elif data.expression_list %}{{ data.expression_list }}{% elif data.columns %}{% for col in data.columns %}{{ conn|qtIdent(col) }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} FROM {{ conn|qtIdent(data.schema, data.table) }}; {% if data.comment %} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/properties.sql index 1e38fd0d1f5..1663a0a757d 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/properties.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/templates/statistics/sql/default/properties.sql @@ -20,7 +20,7 @@ SELECT CASE WHEN 'm' = ANY(s.stxkind) THEN true ELSE false END AS has_mcv, s.stxstattarget AS stattarget, {### stxexprs added in PostgreSQL 14 for expression statistics ###} - s.stxexprs AS expressions, + pg_catalog.pg_get_expr(s.stxexprs, s.stxrelid) AS expression_list, {### Statistics data from pg_statistic_ext_data (PostgreSQL 12+) ###} sd.stxdndistinct AS ndistinct_values, sd.stxddependencies AS dependencies_values, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/statistics_test_data.json b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/statistics_test_data.json index 91e5f2555a7..9ff920e98b4 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/statistics_test_data.json +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/statistics_test_data.json @@ -90,6 +90,29 @@ "test_result_data": {} } }, + { + "name": "Create statistics: With expressions (PG 14+).", + "url": "/browser/statistics/obj/", + "is_positive_test": true, + "inventory_data": { + "server_min_version": 140000, + "skip_msg": "Statistics node not supported below PG 14" + }, + "test_data": { + "name": "test_stats_expr", + "schema": "public", + "table": "test_table", + "expression_list": "lower(col3), (col1 + col2)", + "stat_types": ["ndistinct"] + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "error_msg": null, + "test_result_data": {} + } + }, { "name": "Create statistics: With invalid data - Less than 2 columns.", "url": "/browser/statistics/obj/", diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_delete.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_delete.py index dff34617216..5118d62935c 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_delete.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_delete.py @@ -140,15 +140,22 @@ def runTest(self): self.assertIsNone(statistics_response_2, "Second deleted statistics still present") else: + if 'statistics_id' in self.data: + self.statistics_id = self.data["statistics_id"] if self.mocking_required: with patch( self.mock_data["function_name"], side_effect=[eval(self.mock_data["return_value"])] ): - response = statistics_utils.api_delete(self) - elif 'statistics_id' in self.data: - self.statistics_id = self.data["statistics_id"] - response = statistics_utils.api_delete(self) + response = ( + statistics_utils.api_delete(self, '') + if self.is_list else statistics_utils.api_delete(self) + ) + else: + response = ( + statistics_utils.api_delete(self, '') + if self.is_list else statistics_utils.api_delete(self) + ) # Assert response utils.assert_status_code(self, response) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_get.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_get.py index 6722929c007..d1377a38a5f 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_get.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/test_statistics_get.py @@ -130,16 +130,23 @@ def runTest(self): # Verify stat_types are returned self.assertIn('stat_types', response_data) else: + if 'statistics_id' in self.data: + self.statistics_id = self.data["statistics_id"] + get_call = ( + statistics_utils.api_get(self, '') + if self.is_list else statistics_utils.api_get(self) + ) if self.mocking_required: with patch( self.mock_data["function_name"], side_effect=[eval(self.mock_data["return_value"])] ): - response = statistics_utils.api_get(self) - elif 'statistics_id' in self.data: - # Non-existing statistics id - self.statistics_id = self.data["statistics_id"] - response = statistics_utils.api_get(self) + response = ( + statistics_utils.api_get(self, '') + if self.is_list else statistics_utils.api_get(self) + ) + else: + response = get_call # Assert response utils.assert_status_code(self, response) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/utils.py index 2cc70bb0868..573035f5c30 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/utils.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/statistics/tests/utils.py @@ -190,6 +190,67 @@ def create_statistics(server, db_name, schema_name, table_name, raise +def create_statistics_with_expressions(server, db_name, schema_name, + table_name, statistics_name, + expressions, stat_types): + """ + Creates a statistics object using expressions rather than plain columns. + + Args: + server: server details + db_name: database name + schema_name: schema name + table_name: table name + statistics_name: statistics object name + expressions: list of SQL expression strings + stat_types: list of statistics types (ndistinct, dependencies, mcv) + + Returns: + statistics OID + """ + connection = None + try: + connection = test_utils.get_db_connection( + db_name, + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode'] + ) + old_isolation_level = connection.isolation_level + test_utils.set_isolation_level(connection, 0) + pg_cursor = connection.cursor() + + stat_types_str = ', '.join(stat_types) + exprs_str = ', '.join(f'({e})' for e in expressions) + query = ( + f"CREATE STATISTICS {schema_name}.{statistics_name} " + f"({stat_types_str}) ON {exprs_str} " + f"FROM {schema_name}.{table_name}" + ) + pg_cursor.execute(query) + + pg_cursor.execute( + f"SELECT s.oid FROM pg_catalog.pg_statistic_ext s " + f"JOIN pg_catalog.pg_namespace n ON s.stxnamespace = n.oid " + f"WHERE s.stxname = '{statistics_name}' " + f"AND n.nspname = '{schema_name}'" + ) + statistics = pg_cursor.fetchone() + statistics_oid = statistics[0] + test_utils.set_isolation_level(connection, old_isolation_level) + connection.commit() + + return statistics_oid + except Exception: + traceback.print_exc(file=sys.stderr) + raise + finally: + if connection: + connection.close() + + def verify_statistics(server, db_name, statistics_name): """ This function verifies that a statistics object exists in the database. @@ -319,10 +380,12 @@ def get_statistics_columns(server, db_name, statistics_oid): ) pg_cursor = connection.cursor() pg_cursor.execute( - f"SELECT array_agg(a.attname) FROM pg_catalog.pg_attribute a " - f"JOIN pg_catalog.pg_statistic_ext s ON a.attrelid = s.stxrelid " - f"WHERE s.oid = {statistics_oid} " - f"AND a.attnum = ANY(s.stxkeys)" + f"SELECT array_agg(a.attname ORDER BY s.stxkeys_pos) " + f"FROM pg_catalog.pg_statistic_ext s, " + f"unnest(s.stxkeys) WITH ORDINALITY AS s(attnum, stxkeys_pos) " + f"JOIN pg_catalog.pg_attribute a " + f" ON a.attrelid = s.stxrelid AND a.attnum = s.attnum " + f"WHERE s.oid = {statistics_oid}" ) columns = pg_cursor.fetchone() connection.close()