diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/utils.py index 306c2ef8f1c..0232cf2264c 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/utils.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/utils.py @@ -11,6 +11,7 @@ from flask import render_template from flask_babel import gettext as _ +from pgadmin.utils import unquote_ident from pgadmin.utils.ajax import internal_server_error from pgadmin.utils.exception import ObjectGone, ExecuteError from functools import wraps @@ -74,7 +75,7 @@ def _get_columns(res): order = True nulls_order = True if (row['options'] & 2) else False - columns.append({"column": row['coldef'].strip('"'), + columns.append({"column": unquote_ident(row['coldef']), "oper_class": row['opcname'], "order": order, "nulls_order": nulls_order, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/utils.py index cd70f40e20e..76dab362980 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/utils.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/utils.py @@ -11,6 +11,7 @@ from flask import render_template from flask_babel import gettext as _ +from pgadmin.utils import unquote_ident from pgadmin.utils.ajax import internal_server_error from pgadmin.utils.exception import ObjectGone, ExecuteError from functools import wraps @@ -126,7 +127,7 @@ def search_coveringindex(conn, tid, cols, template_path=None): index_cols = set() for r in rest['rows']: - index_cols.add(r['column'].strip('"')) + index_cols.add(unquote_ident(r['column'])) if len(cols - index_cols) == len(index_cols - cols) == 0: return constraint["idxname"] diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/__init__.py index 387a7355fc5..445475dc9f5 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/__init__.py @@ -18,6 +18,7 @@ from pgadmin.browser.server_groups.servers.databases.schemas.tables.\ constraints.type import ConstraintRegistry, ConstraintTypeModule from pgadmin.browser.utils import PGChildNodeView +from pgadmin.utils import unquote_ident from pgadmin.utils.ajax import make_json_response, internal_server_error, \ make_response as ajax_response, gone from pgadmin.browser.server_groups.servers.databases.schemas.tables.\ @@ -878,7 +879,7 @@ def sql(self, gid, sid, did, scid, tid, cid=None): columns = [] for row in res['rows']: - columns.append({"column": row['column'].strip('"')}) + columns.append({"column": unquote_ident(row['column'])}) data['columns'] = columns diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/utils.py index 8d7ae8460a4..b3860dd919b 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/utils.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/utils.py @@ -11,6 +11,7 @@ from flask import render_template from flask_babel import gettext as _ +from pgadmin.utils import unquote_ident from pgadmin.utils.ajax import internal_server_error from pgadmin.utils.exception import ObjectGone, ExecuteError from functools import wraps @@ -90,7 +91,7 @@ def get_index_constraints(conn, did, tid, ctype, cid=None, template_path=None): columns = [] for r in res['rows']: - columns.append({"column": r['column'].strip('"')}) + columns.append({"column": unquote_ident(r['column'])}) idx_cons['columns'] = columns diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/tests/test_indexes_quoted_column.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/tests/test_indexes_quoted_column.py new file mode 100644 index 00000000000..366ad6bb527 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/tests/test_indexes_quoted_column.py @@ -0,0 +1,114 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2026, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Regression test for index columns whose names require quoting (#6481). + +pg_get_indexdef() returns such a name quoted, so comparing it against +pg_attribute.attname classified the column as an expression and the +Properties panel showed nothing at all. The SQL now compares against +quote_ident(attname), and the name is unquoted properly on the way out +rather than by stripping quote characters, which mangled any name +containing a literal double quote. +""" + +import uuid + +from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \ + import utils as tables_utils +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.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from . import utils as indexes_utils + + +class IndexesQuotedColumnTestCase(BaseTestGenerator): + """An index on a column needing quotes must report that column.""" + + url = "/browser/index/obj/" + + scenarios = [ + ('Mixed case column name', dict( + column_name='Mixed Case', + )), + ('Column name containing a double quote', dict( + column_name='col"x', + )), + ('Column name that is a reserved word', dict( + column_name='select', + )), + ] + + 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 a table.") + 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 a table.") + + self.table_name = "table_quoted_col_%s" % (str(uuid.uuid4())[1:8]) + self.table_id = tables_utils.create_table(self.server, self.db_name, + self.schema_name, + self.table_name) + + # The helpers interpolate names into SQL as given, so quote the + # column exactly as the server would. + quoted_column = '"%s"' % self.column_name.replace('"', '""') + self._add_column(quoted_column) + + self.index_name = "test_index_quoted_%s" % (str(uuid.uuid4())[1:8]) + self.index_id = indexes_utils.create_index( + self.server, self.db_name, self.schema_name, self.table_name, + self.index_name, quoted_column) + + def _add_column(self, quoted_column): + connection = utils.get_db_connection(self.db_name, + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port'], + self.server['sslmode']) + old_isolation_level = connection.isolation_level + utils.set_isolation_level(connection, 0) + pg_cursor = connection.cursor() + pg_cursor.execute('ALTER TABLE %s.%s ADD COLUMN %s text' % ( + self.schema_name, self.table_name, quoted_column)) + utils.set_isolation_level(connection, old_isolation_level) + connection.commit() + connection.close() + + def runTest(self): + response = indexes_utils.api_get_index(self, self.index_id) + self.assertEqual(response.status_code, 200) + + data = response.json + self.assertEqual(len(data['columns']), 1) + column = data['columns'][0] + + # The name must come back exactly as the user typed it, and must not + # be mistaken for an expression. + self.assertEqual(column['colname'], self.column_name) + self.assertFalse(column['is_exp']) + + def tearDown(self): + database_utils.disconnect_database(self, self.server_id, self.db_id) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/utils.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/utils.py index 04e6f32ec79..f57d5f3151d 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/utils.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/utils.py @@ -11,6 +11,7 @@ from flask import render_template from flask_babel import gettext +from pgadmin.utils import unquote_ident from pgadmin.utils.ajax import internal_server_error from pgadmin.utils.exception import ObjectGone, ExecuteError from functools import wraps @@ -120,7 +121,7 @@ def get_column_details(conn, idx, data, mode='properties', template_path=None): # we will not strip down colname when using in SQL to display cols_data = { 'colname': row['attdef'] if mode == 'create' else - row['attdef'].strip('"'), + unquote_ident(row['attdef']), 'collspcname': row['collnspname'], 'op_class': row['opcname'], 'col_num': row['attnum'], diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/default/get_constraint_cols.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/default/get_constraint_cols.sql index 325656de814..a621dac5fad 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/default/get_constraint_cols.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/exclusion_constraint/sql/default/get_constraint_cols.sql @@ -12,7 +12,7 @@ SELECT coll.collname, nspc.nspname as collnspname, pg_catalog.format_type(ty.oid,NULL) AS datatype, - CASE WHEN pg_catalog.pg_get_indexdef(i.indexrelid, {{loop.index}}, true) = a.attname THEN FALSE ELSE TRUE END AS is_exp + CASE WHEN pg_catalog.pg_get_indexdef(i.indexrelid, {{loop.index}}, true) = pg_catalog.quote_ident(a.attname) THEN FALSE ELSE TRUE END AS is_exp FROM pg_catalog.pg_index i JOIN pg_catalog.pg_attribute a ON (a.attrelid = i.indexrelid AND attnum = {{loop.index}}) JOIN pg_catalog.pg_type ty ON ty.oid=a.atttypid diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/indexes/sql/default/column_details.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/indexes/sql/default/column_details.sql index 05c468df5f2..cc53e6877c3 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/indexes/sql/default/column_details.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/indexes/sql/default/column_details.sql @@ -9,7 +9,7 @@ SELECT END::text[] AS options, i.attnum, pg_catalog.pg_get_indexdef(i.indexrelid, i.attnum, true) as attdef, - CASE WHEN pg_catalog.pg_get_indexdef(i.indexrelid, i.attnum, true) = a.attname THEN FALSE ELSE TRUE END AS is_exp, + CASE WHEN pg_catalog.pg_get_indexdef(i.indexrelid, i.attnum, true) = pg_catalog.quote_ident(a.attname) THEN FALSE ELSE TRUE END AS is_exp, a.attstattarget as statistics, CASE WHEN (o.opcdefault = FALSE) THEN o.opcname ELSE null END AS opcname, op.oprname AS oprname, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_add_cols.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_add_cols.sql index a5564eebc9f..ac25458ce89 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_add_cols.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_add_cols.sql @@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS public."simple_table_with_pk$%{}[]()&*^!@""'`\/#" "col2_$%{}[]()&*^!@\""'`\\/#" json NOT NULL, "col3_$%{}[]()&*^!@\""'`\\/#" numeric(10,5), "col4_$%{}[]()&*^!@\""'`\\/#" text COLLATE pg_catalog."default", - CONSTRAINT "simple_table_with_pk$%{}[]()&*^!@""'`\/#_pkey" PRIMARY KEY ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT "simple_table_with_pk$%{}[]()&*^!@""'`\/#_pkey" PRIMARY KEY ("col1_$%{}[]()&*^!@\""'`\\/#") ) TABLESPACE pg_default; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_add_unique_const.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_add_unique_const.sql index d082d1d31c9..5f72f072d6a 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_add_unique_const.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_add_unique_const.sql @@ -6,10 +6,10 @@ CREATE TABLE IF NOT EXISTS public."table_with_pk_chk_constraints$%{}[]()&*^!@""' ( "col1_$%{}[]()&*^!@\""'`\\/#" time(5) with time zone NOT NULL, col2 character(12) COLLATE pg_catalog."default", - CONSTRAINT custom_pk PRIMARY KEY ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT custom_pk PRIMARY KEY ("col1_$%{}[]()&*^!@\""'`\\/#") WITH (FILLFACTOR=11) DEFERRABLE INITIALLY DEFERRED, - CONSTRAINT "unique" UNIQUE ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT "unique" UNIQUE ("col1_$%{}[]()&*^!@\""'`\\/#") WITH (FILLFACTOR=13), CONSTRAINT chk_const CHECK (col2 <> NULL::bpchar) ) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_delete_cols.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_delete_cols.sql index 5c5f52eb025..669b3f7d8ff 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_delete_cols.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_delete_cols.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS public."simple_table_with_pk$%{}[]()&*^!@""'`\/#" "col1_$%{}[]()&*^!@\""'`\\/#" integer NOT NULL, "col3_$%{}[]()&*^!@\""'`\\/#" numeric(10,5), "col4_$%{}[]()&*^!@\""'`\\/#" text COLLATE pg_catalog."default", - CONSTRAINT "simple_table_with_pk$%{}[]()&*^!@""'`\/#_pkey" PRIMARY KEY ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT "simple_table_with_pk$%{}[]()&*^!@""'`\/#_pkey" PRIMARY KEY ("col1_$%{}[]()&*^!@\""'`\\/#") ) TABLESPACE pg_default; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_delete_constraints.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_delete_constraints.sql index 101d0ca4aa4..02edb4c28ac 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_delete_constraints.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/alter_table_delete_constraints.sql @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS public."table_with_pk_chk_constraints$%{}[]()&*^!@""' ( "col1_$%{}[]()&*^!@\""'`\\/#" time(5) with time zone NOT NULL, col2 character(12) COLLATE pg_catalog."default", - CONSTRAINT custom_pk PRIMARY KEY ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT custom_pk PRIMARY KEY ("col1_$%{}[]()&*^!@\""'`\\/#") WITH (FILLFACTOR=11) DEFERRABLE INITIALLY DEFERRED ) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/create_table_with_pk.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/create_table_with_pk.sql index 95bd2896138..f1153da0c64 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/create_table_with_pk.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/create_table_with_pk.sql @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS public."simple_table_with_pk$%{}[]()&*^!@""'`\/#" ( "col1_$%{}[]()&*^!@\""'`\\/#" integer NOT NULL, "col2_$%{}[]()&*^!@\""'`\\/#" json NOT NULL, - CONSTRAINT "simple_table_with_pk$%{}[]()&*^!@""'`\/#_pkey" PRIMARY KEY ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT "simple_table_with_pk$%{}[]()&*^!@""'`\/#_pkey" PRIMARY KEY ("col1_$%{}[]()&*^!@\""'`\\/#") ) TABLESPACE pg_default; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/create_table_with_pk_chk.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/create_table_with_pk_chk.sql index 8db3c072f97..f85a9aaa802 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/create_table_with_pk_chk.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/pg/default/create_table_with_pk_chk.sql @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS public."table_with_pk_chk_constraints$%{}[]()&*^!@""' ( "col1_$%{}[]()&*^!@\""'`\\/#" time(5) with time zone NOT NULL, col2 character(12) COLLATE pg_catalog."default", - CONSTRAINT custom_pk PRIMARY KEY ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT custom_pk PRIMARY KEY ("col1_$%{}[]()&*^!@\""'`\\/#") WITH (FILLFACTOR=11) DEFERRABLE INITIALLY DEFERRED, CONSTRAINT chk_const CHECK (col2 <> NULL::bpchar) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_add_cols.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_add_cols.sql index c203a7b14af..75f402ee3bc 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_add_cols.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_add_cols.sql @@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS public."simple_table_with_pk$%{}[]()&*^!@""'`\/#" "col2_$%{}[]()&*^!@\""'`\\/#" json NOT NULL, "col3_$%{}[]()&*^!@\""'`\\/#" numeric(10,5), "col4_$%{}[]()&*^!@\""'`\\/#" text COLLATE pg_catalog."default", - CONSTRAINT "simple_table_with_pk$%{}[]()&*^!@""'`\/#_pkey" PRIMARY KEY ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT "simple_table_with_pk$%{}[]()&*^!@""'`\/#_pkey" PRIMARY KEY ("col1_$%{}[]()&*^!@\""'`\\/#") ) TABLESPACE pg_default; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_add_unique_const.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_add_unique_const.sql index 4d241798454..f76f4d44906 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_add_unique_const.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_add_unique_const.sql @@ -6,10 +6,10 @@ CREATE TABLE IF NOT EXISTS public."table_with_pk_chk_constraints$%{}[]()&*^!@""' ( "col1_$%{}[]()&*^!@\""'`\\/#" time(5) with time zone NOT NULL, col2 character(12) COLLATE pg_catalog."default", - CONSTRAINT custom_pk PRIMARY KEY ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT custom_pk PRIMARY KEY ("col1_$%{}[]()&*^!@\""'`\\/#") WITH (FILLFACTOR=11) DEFERRABLE INITIALLY DEFERRED, - CONSTRAINT "unique" UNIQUE ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT "unique" UNIQUE ("col1_$%{}[]()&*^!@\""'`\\/#") WITH (FILLFACTOR=13), CONSTRAINT chk_const CHECK (col2 <> NULL::bpchar) ) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_delete_cols.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_delete_cols.sql index 75c7303a4d7..00e65d1d7c2 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_delete_cols.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_delete_cols.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS public."simple_table_with_pk$%{}[]()&*^!@""'`\/#" "col1_$%{}[]()&*^!@\""'`\\/#" integer NOT NULL, "col3_$%{}[]()&*^!@\""'`\\/#" numeric(10,5), "col4_$%{}[]()&*^!@\""'`\\/#" text COLLATE pg_catalog."default", - CONSTRAINT "simple_table_with_pk$%{}[]()&*^!@""'`\/#_pkey" PRIMARY KEY ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT "simple_table_with_pk$%{}[]()&*^!@""'`\/#_pkey" PRIMARY KEY ("col1_$%{}[]()&*^!@\""'`\\/#") ) TABLESPACE pg_default; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_delete_constraints.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_delete_constraints.sql index 64088340730..dee8a5114fd 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_delete_constraints.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/alter_table_delete_constraints.sql @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS public."table_with_pk_chk_constraints$%{}[]()&*^!@""' ( "col1_$%{}[]()&*^!@\""'`\\/#" time(5) with time zone NOT NULL, col2 character(12) COLLATE pg_catalog."default", - CONSTRAINT custom_pk PRIMARY KEY ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT custom_pk PRIMARY KEY ("col1_$%{}[]()&*^!@\""'`\\/#") WITH (FILLFACTOR=11) DEFERRABLE INITIALLY DEFERRED ) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/create_table_with_pk.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/create_table_with_pk.sql index 4fa40293897..fedf0402db6 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/create_table_with_pk.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/create_table_with_pk.sql @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS public."simple_table_with_pk$%{}[]()&*^!@""'`\/#" ( "col1_$%{}[]()&*^!@\""'`\\/#" integer NOT NULL, "col2_$%{}[]()&*^!@\""'`\\/#" json NOT NULL, - CONSTRAINT "simple_table_with_pk$%{}[]()&*^!@""'`\/#_pkey" PRIMARY KEY ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT "simple_table_with_pk$%{}[]()&*^!@""'`\/#_pkey" PRIMARY KEY ("col1_$%{}[]()&*^!@\""'`\\/#") ) TABLESPACE pg_default; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/create_table_with_pk_chk.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/create_table_with_pk_chk.sql index ca9ae87bbe2..6239e362c9b 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/create_table_with_pk_chk.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/ppas/default/create_table_with_pk_chk.sql @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS public."table_with_pk_chk_constraints$%{}[]()&*^!@""' ( "col1_$%{}[]()&*^!@\""'`\\/#" time(5) with time zone NOT NULL, col2 character(12) COLLATE pg_catalog."default", - CONSTRAINT custom_pk PRIMARY KEY ("col1_$%{}[]()&*^!@\""""'`\\/#") + CONSTRAINT custom_pk PRIMARY KEY ("col1_$%{}[]()&*^!@\""'`\\/#") WITH (FILLFACTOR=11) DEFERRABLE INITIALLY DEFERRED, CONSTRAINT chk_const CHECK (col2 <> NULL::bpchar) diff --git a/web/pgadmin/utils/__init__.py b/web/pgadmin/utils/__init__.py index 0a57d7e6c0f..a60d834fcce 100644 --- a/web/pgadmin/utils/__init__.py +++ b/web/pgadmin/utils/__init__.py @@ -212,6 +212,31 @@ def document_dir(): return os.path.realpath(os.path.expanduser('~/')) +# A single SQL identifier, quoted, with every embedded double quote doubled. +_QUOTED_IDENT = re.compile(r'"(?:[^"]|"")*"\Z') + + +def unquote_ident(value): + """ + Reverse the quoting that Driver.qtIdent() and the server's own + quote_ident() apply to an identifier. + + Catalogue functions such as pg_get_indexdef() return an identifier quoted + only when it needs to be, with any embedded double quote doubled, so a + column named 'col"x' arrives as '"col""x"'. Stripping the outer quotes + alone would leave that doubled quote behind. + + Anything that is not a single quoted identifier, an unquoted name or an + expression such as '(a || b)', is returned unchanged. + + :param value: identifier as returned by the server + :return: the identifier as the user typed it + """ + if value and _QUOTED_IDENT.match(value): + return value[1:-1].replace('""', '"') + return value + + def get_directory_and_file_name(drivefilepath): """ Returns directory name if specified and file name diff --git a/web/pgadmin/utils/tests/test_unquote_ident.py b/web/pgadmin/utils/tests/test_unquote_ident.py new file mode 100644 index 00000000000..05c7185fffa --- /dev/null +++ b/web/pgadmin/utils/tests/test_unquote_ident.py @@ -0,0 +1,56 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2026, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Tests for unquote_ident(). + +Identifiers arrive from catalogue functions such as pg_get_indexdef() quoted +only when they need to be, with any embedded double quote doubled. The +previous str.strip('"') removed the outer quotes but left the doubled ones +behind, so a column named 'col"x' was displayed as 'col""x' (#6481). An +expression must survive untouched, which strip() also failed at. +""" + +from pgadmin.utils import unquote_ident +from pgadmin.utils.route import BaseTestGenerator + + +class UnquoteIdentTestCase(BaseTestGenerator): + """unquote_ident() must reverse quote_ident() and leave the rest alone.""" + + scenarios = [ + ('An unquoted name is returned as is', + dict(value='colname', expected='colname')), + ('Outer quotes are removed', + dict(value='"Col"', expected='Col')), + ('A doubled inner quote is unescaped', + dict(value='"col""x"', expected='col"x')), + ('Several doubled inner quotes are unescaped', + dict(value='"a""b""c"', expected='a"b"c')), + ('A name that is nothing but quotes is unescaped', + dict(value='""""', expected='"')), + ('A quoted name containing spaces keeps them', + dict(value='"my column"', expected='my column')), + ('An unquoted expression is untouched', + dict(value='(a || b)', expected='(a || b)')), + ('An expression of quoted names is untouched', + dict(value='"a" || "b"', expected='"a" || "b"')), + ('A lone quote is untouched', + dict(value='"', expected='"')), + ('An empty string is untouched', + dict(value='', expected='')), + ('None is untouched', + dict(value=None, expected=None)), + ] + + def setUp(self): + # A pure string function: no server connection required. + pass + + def runTest(self): + self.assertEqual(unquote_ident(self.value), self.expected)