From 8e95bb45e1748af8fcaaa1b42a5195bd7387e400 Mon Sep 17 00:00:00 2001 From: Kundan Sable Date: Wed, 22 Jul 2026 14:20:00 +0530 Subject: [PATCH 1/2] fix: exclude non-editable/alias columns from Query Tool row UPDATE The added (INSERT) branch of save_changed_data() already filtered out columns that aren't backed by a real table column (added for #9939 / #10015), but the updated (UPDATE) branch did not. Editing a row that includes an aliased/expression column (e.g. first_name || ' ' || last_name AS the_name) sent that alias into the generated UPDATE's SET clause, which fails with "column ... does not exist" even though the grid already marks the column read-only. Apply the same editable-columns guard to the updated branch, and skip the row entirely if nothing editable remains after filtering. Fixes #10103 --- .../tools/sqleditor/utils/save_changed_data.py | 15 +++++++++++++++ 1 file changed, 15 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..3965daa9aa9 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 that isn't a real editable column of the + # underlying table (e.g. an expression/alias column such as + # `first_name || ' ' || last_name as the_name`). Such columns + # carry the read-only lock icon in the grid, but without this + # guard the rendered UPDATE references a non-existent column + # and Postgres rejects the change. Issue #10103. + data = { + k: v for k, v in data.items() + if k in columns_info and + columns_info[k].get('is_editable', True) + } + # Nothing editable left to persist for this row, skip it so we + # don't render an invalid `SET` clause. + if not data: + continue pk_escaped = { pk: pk_val.replace('%', '%%') if hasattr( pk_val, 'replace') else pk_val From e15b0a8ba37769e5abee3ccdfe4097d5b519d493 Mon Sep 17 00:00:00 2001 From: Kundan Sable Date: Fri, 24 Jul 2026 19:40:27 +0530 Subject: [PATCH 2/2] fix: address review feedback on updated-row column filtering Per @asheshv's review on PR #10171: 1. The added-row path explicitly pops the client_primary_key/ is_row_copied tracking keys before filtering to editable columns; the updated-row path relied implicitly on the columns_info filter to drop them, since neither key is ever present on an updated-row payload today. Mirror the explicit pops for symmetry, so this path isn't silently relying on that assumption if a future change starts tagging updated rows the same way (e.g. multi-row copy-paste into existing rows). 2. 'row_id' was read via data.get(client_primary_key) on the already-filtered dict, so it was always None - dead weight now, and actively wrong once point 1 makes the filtering explicit rather than incidental. Capture row_id from the row's data before it's popped/filtered, so a failed update's error report can identify which row failed. --- .../sqleditor/utils/save_changed_data.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/web/pgadmin/tools/sqleditor/utils/save_changed_data.py b/web/pgadmin/tools/sqleditor/utils/save_changed_data.py index 3965daa9aa9..adf8999c918 100644 --- a/web/pgadmin/tools/sqleditor/utils/save_changed_data.py +++ b/web/pgadmin/tools/sqleditor/utils/save_changed_data.py @@ -177,6 +177,22 @@ 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'] + # client_primary_key is a synthetic tracking key (e.g. + # '__temp_PK') chosen specifically to never match a real + # column name, so it must be read before it is popped/ + # filtered out below. + row_id = data.get(client_primary_key) + # Remove our unique tracking keys, mirroring the + # added-row path above. Today neither key is ever + # present on an updated-row payload (only the added-row + # path tags rows this way), so the columns_info filter + # below is already sufficient - but stripping them + # explicitly keeps this path from silently relying on + # that assumption if a future change starts tagging + # updated rows the same way (e.g. multi-row copy-paste + # into existing rows). + data.pop(client_primary_key, None) + data.pop('is_row_copied', None) # Drop any column that isn't a real editable column of the # underlying table (e.g. an expression/alias column such as # `first_name || ' ' || last_name as the_name`). Such columns @@ -211,8 +227,7 @@ def save_changed_data(changed_data, columns_info, conn, command_obj, ) list_of_sql[of_type].append({'sql': sql, 'data': data, - 'row_id': - data.get(client_primary_key)}) + 'row_id': row_id}) # For deleted rows elif of_type == 'deleted':