From d6339e0e82709c0e82e3e0aee487f088149acf5c Mon Sep 17 00:00:00 2001 From: Dave Page Date: Wed, 19 Aug 2026 13:04:11 +0100 Subject: [PATCH] fix: exclude non-editable alias/expression columns from UPDATE saves in Query Tool (#10103) --- .../sqleditor/utils/save_changed_data.py | 15 ++++ .../utils/tests/test_save_changed_data.py | 84 +++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/web/pgadmin/tools/sqleditor/utils/save_changed_data.py b/web/pgadmin/tools/sqleditor/utils/save_changed_data.py index 78776697ff3..c9c72316d8a 100644 --- a/web/pgadmin/tools/sqleditor/utils/save_changed_data.py +++ b/web/pgadmin/tools/sqleditor/utils/save_changed_data.py @@ -177,6 +177,21 @@ def save_changed_data(changed_data, columns_info, conn, command_obj, list_of_sql[of_type] = [] for each_row in changed_data[of_type]: data = changed_data[of_type][each_row]['data'] + + # Drop any column the client included that isn't a real + # editable column of the underlying table (e.g. + # `first_name || ' ' || last_name as the_name`). The + # frontend already marks such columns as non-editable + # (shown with a lock icon), but still includes them in + # the changed data. Without this guard the rendered + # UPDATE references a non-existent column and Postgres + # rejects it. Issue #10103. + data = { + k: v for k, v in data.items() + if k in columns_info and + columns_info[k].get('is_editable', True) + } + pk_escaped = { pk: pk_val.replace('%', '%%') if hasattr( pk_val, 'replace') else pk_val diff --git a/web/pgadmin/tools/sqleditor/utils/tests/test_save_changed_data.py b/web/pgadmin/tools/sqleditor/utils/tests/test_save_changed_data.py index b43d0773a9a..6d5c79c0c2c 100644 --- a/web/pgadmin/tools/sqleditor/utils/tests/test_save_changed_data.py +++ b/web/pgadmin/tools/sqleditor/utils/tests/test_save_changed_data.py @@ -1040,3 +1040,87 @@ def _create_test_table(self): "FROM {0};" ).format(self.test_table_name) utils.create_table_with_query(self.server, self.db_name, create_sql) + + +class TestSaveUpdatedRowSkipsNonEditableColumn(TestSaveChangedData): + """Regression test for issue #10103. + + When a Query Tool result includes an expression or alias column + (e.g. ``first_name || ' ' || last_name AS the_name``), editing a + real column on an existing row must not include the alias in the + generated UPDATE statement. The alias is not a real column of the + underlying table, so pgAdmin already flags it as non-editable (shown + with a lock icon in the grid) but the update save flow used to send + it anyway, causing PostgreSQL to reject the statement with + ``column "the_name" does not exist``. + """ + + scenarios = [ + ('Update a real column while an aliased expression column is ' + 'present', dict( + save_payload={ + "updated": { + "1": { + "err": False, + "data": { + "first_name": "Jane", + # The client includes the aliased expression + # column in the changed data even though it + # is marked non-editable. Sending it must + # not break the UPDATE. + "the_name": "Jane Doe" + }, + "primary_keys": {"id": 1} + } + }, + "added": {}, + "staged_rows": {}, + "deleted": {}, + "updated_index": {"1": "1"}, + "added_index": {}, + "columns": [ + {"name": "id", "pos": 0, "can_edit": True, + "type": "integer", "cell": "number", + "not_null": True, "has_default_val": False, + "is_array": False, "display_name": "id"}, + {"name": "first_name", "pos": 1, "can_edit": True, + "type": "text", "cell": "string", + "not_null": False, "has_default_val": False, + "is_array": False, "display_name": "first_name"}, + {"name": "last_name", "pos": 2, "can_edit": True, + "type": "text", "cell": "string", + "not_null": False, "has_default_val": False, + "is_array": False, "display_name": "last_name"}, + {"name": "the_name", "pos": 3, "can_edit": False, + "type": "text", "cell": "string", + "not_null": False, "has_default_val": False, + "is_array": False, "display_name": "the_name"}, + ] + }, + save_status=True, + check_sql='SELECT id, first_name, last_name ' + 'FROM %s WHERE id = 1', + check_result=[[1, "Jane", "Doe"]] + )), + ] + + def _create_test_table(self): + self.test_table_name = "test_for_save_data_alias_" + \ + str(secrets.choice(range(1000, 9999))) + create_sql = """ + DROP TABLE IF EXISTS "{0}"; + + CREATE TABLE "{0}"( + id INT PRIMARY KEY, + first_name TEXT, + last_name TEXT + ); + + INSERT INTO "{0}" VALUES (1, 'John', 'Doe'); + """.format(self.test_table_name) + self.select_sql = ( + "SELECT id, first_name, last_name, " + "first_name || ' ' || last_name AS the_name " + "FROM {0};" + ).format(self.test_table_name) + utils.create_table_with_query(self.server, self.db_name, create_sql)