Skip to content

Commit 359942f

Browse files
Fixed View/Edit Data not handling generated columns properly. #9672
1 parent 01c2d12 commit 359942f

File tree

5 files changed

+118
-10
lines changed

5 files changed

+118
-10
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
SELECT DISTINCT att.attname as name, att.attnum as OID, pg_catalog.format_type(ty.oid,NULL) AS datatype,
2+
pg_catalog.format_type(ty.oid,att.atttypmod) AS displaytypname,
3+
att.attnotnull as not_null,
4+
CASE WHEN att.atthasdef OR att.attidentity != '' OR ty.typdefault IS NOT NULL THEN True
5+
ELSE False END as has_default_val, des.description, seq.seqtypid,
6+
{# Detect generated columns to exclude from INSERT/UPDATE in View/Edit Data #}
7+
CASE WHEN att.attgenerated = 's' THEN true ELSE false END as is_generated
8+
FROM pg_catalog.pg_attribute att
9+
JOIN pg_catalog.pg_type ty ON ty.oid=atttypid
10+
JOIN pg_catalog.pg_namespace tn ON tn.oid=ty.typnamespace
11+
JOIN pg_catalog.pg_class cl ON cl.oid=att.attrelid
12+
JOIN pg_catalog.pg_namespace na ON na.oid=cl.relnamespace
13+
LEFT OUTER JOIN pg_catalog.pg_type et ON et.oid=ty.typelem
14+
LEFT OUTER JOIN pg_catalog.pg_attrdef def ON adrelid=att.attrelid AND adnum=att.attnum
15+
LEFT OUTER JOIN (pg_catalog.pg_depend JOIN pg_catalog.pg_class cs ON classid='pg_class'::regclass AND objid=cs.oid AND cs.relkind='S') ON refobjid=att.attrelid AND refobjsubid=att.attnum
16+
LEFT OUTER JOIN pg_catalog.pg_namespace ns ON ns.oid=cs.relnamespace
17+
LEFT OUTER JOIN pg_catalog.pg_index pi ON pi.indrelid=att.attrelid AND indisprimary
18+
LEFT OUTER JOIN pg_catalog.pg_description des ON (des.objoid=att.attrelid AND des.objsubid=att.attnum AND des.classoid='pg_class'::regclass)
19+
LEFT OUTER JOIN pg_catalog.pg_sequence seq ON cs.oid=seq.seqrelid
20+
WHERE
21+
22+
{% if tid %}
23+
att.attrelid = {{ tid|qtLiteral(conn) }}::oid
24+
{% endif %}
25+
{% if table_name and table_nspname %}
26+
cl.relname= {{table_name |qtLiteral(conn)}} and na.nspname={{table_nspname|qtLiteral(conn)}}
27+
{% endif %}
28+
{% if clid %}
29+
AND att.attnum = {{ clid|qtLiteral(conn) }}
30+
{% endif %}
31+
{### To show system objects ###}
32+
{% if not show_sys_objects and not has_oids %}
33+
AND att.attnum > 0
34+
{% endif %}
35+
{### To show oids in view data ###}
36+
{% if has_oids %}
37+
AND (att.attnum > 0 OR (att.attname = 'oid' AND att.attnum < 0))
38+
{% endif %}
39+
AND att.attisdropped IS FALSE
40+
ORDER BY att.attnum

web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,8 +1300,10 @@ export function ResultSet() {
13001300
}
13011301

13021302
pageDataOutOfSync.current = true;
1303-
if(_.size(dataChangeStore.added)) {
1304-
// Update the rows in a grid after addition
1303+
// Update the rows in a grid after addition/update.
1304+
// row_added contains refetched row data with recalculated
1305+
// generated column values (for both INSERT and UPDATE).
1306+
if(_.size(dataChangeStore.added) || _.size(dataChangeStore.updated)) {
13051307
respData.data.query_results.forEach((qr)=>{
13061308
if(!_.isNull(qr.row_added)) {
13071309
let rowClientPK = Object.keys(qr.row_added)[0];

web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/update.sql

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ UPDATE {{ conn|qtIdent(nsp_name, object_name) | replace("%", "%%") }} SET
44
{% if not loop.first %}, {% endif %}{{ conn|qtIdent(col) | replace("%", "%%") }} = %({{ pgadmin_alias[col] }})s{% if type_cast_required[col] %}::{{ data_type[col] }}{% endif %}{% endfor %}
55
WHERE
66
{% for pk in primary_keys %}
7-
{% if not loop.first %} AND {% endif %}{{ conn|qtIdent(pk) | replace("%", "%%") }} = {{ primary_keys[pk]|qtLiteral(conn) }}{% endfor %};
7+
{% if not loop.first %} AND {% endif %}{{ conn|qtIdent(pk) | replace("%", "%%") }} = {{ primary_keys[pk]|qtLiteral(conn) }}{% endfor %}
8+
{# Return primary keys to refetch row with recalculated generated column values #}
9+
{% if pk_names and not has_oids %} RETURNING {{pk_names | replace("%", "%%")}}{% endif %}
10+
{% if has_oids %} RETURNING oid{% endif %};

web/pgadmin/tools/sqleditor/utils/get_column_types.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ def get_columns_types(is_query_tool, columns_info, table_oid, conn, has_oids,
6464
col_type['seqtypid'] = col['seqtypid'] = \
6565
rset['rows'][key]['seqtypid']
6666

67+
# Check if column is a generated column (PostgreSQL 12+).
68+
# Generated columns must be excluded from INSERT/UPDATE.
69+
col_type['is_generated'] = col['is_generated'] = \
70+
rset['rows'][key].get('is_generated', False)
71+
6772
else:
6873
for row in rset['rows']:
6974
if row['oid'] == col['table_column']:
@@ -76,12 +81,17 @@ def get_columns_types(is_query_tool, columns_info, table_oid, conn, has_oids,
7681

7782
col_type['seqtypid'] = col['seqtypid'] = \
7883
row['seqtypid']
84+
85+
# Check if column is a generated column (PG 12+).
86+
col_type['is_generated'] = col['is_generated'] = \
87+
row.get('is_generated', False)
7988
break
8089

8190
else:
8291
col_type['not_null'] = col['not_null'] = None
8392
col_type['has_default_val'] = \
8493
col['has_default_val'] = None
8594
col_type['seqtypid'] = col['seqtypid'] = None
95+
col_type['is_generated'] = col['is_generated'] = False
8696

8797
return column_types

web/pgadmin/tools/sqleditor/utils/save_changed_data.py

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ def save_changed_data(changed_data, columns_info, conn, command_obj,
118118
if command_obj.has_oids():
119119
data.pop('oid', None)
120120

121+
# Remove generated columns (GENERATED ALWAYS AS) as they
122+
# cannot be inserted - PostgreSQL auto-computes their values.
123+
for col_name, col_info in columns_info.items():
124+
if col_info.get('is_generated', False):
125+
data.pop(col_name, None)
126+
121127
# Update columns value with columns having
122128
# not_null=False and has no default value
123129
column_data.update(data)
@@ -163,14 +169,38 @@ def save_changed_data(changed_data, columns_info, conn, command_obj,
163169
# For updated rows
164170
elif of_type == 'updated':
165171
list_of_sql[of_type] = []
172+
173+
# Check if table has generated columns. If yes, we need to
174+
# refetch row after UPDATE to get recalculated values for UI.
175+
has_generated_cols = any(
176+
col_info.get('is_generated', False)
177+
for col_info in columns_info.values()
178+
)
179+
180+
# Get primary keys info (same as INSERT) - needed for RETURNING
181+
# clause and SELECT query to refetch updated row.
182+
pk_names, primary_keys = command_obj.get_primary_keys()
183+
166184
for each_row in changed_data[of_type]:
167185
data = changed_data[of_type][each_row]['data']
186+
row_primary_keys = changed_data[of_type][each_row][
187+
'primary_keys']
188+
189+
# Remove generated columns (GENERATED ALWAYS AS) as they
190+
# cannot be updated - PostgreSQL auto-computes their values.
191+
for col_name, col_info in columns_info.items():
192+
if col_info.get('is_generated', False):
193+
data.pop(col_name, None)
194+
168195
pk_escaped = {
169196
pk: pk_val.replace('%', '%%') if hasattr(
170197
pk_val, 'replace') else pk_val
171-
for pk, pk_val in
172-
changed_data[of_type][each_row]['primary_keys'].items()
198+
for pk, pk_val in row_primary_keys.items()
173199
}
200+
201+
# Pass pk_names and has_oids for RETURNING clause in
202+
# UPDATE statement.
203+
# This will help to fetch the updated row's.
174204
sql = render_template(
175205
"/".join([command_obj.sql_path, 'update.sql']),
176206
data_to_be_saved=data,
@@ -180,12 +210,35 @@ def save_changed_data(changed_data, columns_info, conn, command_obj,
180210
nsp_name=command_obj.nsp_name,
181211
data_type=column_type,
182212
type_cast_required=type_cast_required,
213+
pk_names=pk_names if has_generated_cols else None,
214+
has_oids=command_obj.has_oids(),
183215
conn=conn
184216
)
185-
list_of_sql[of_type].append({'sql': sql,
186-
'data': data,
187-
'row_id':
188-
data.get(client_primary_key)})
217+
218+
# For tables with generated columns, add select_sql to
219+
# refetch updated row.
220+
if has_generated_cols:
221+
select_sql = render_template(
222+
"/".join([command_obj.sql_path, 'select.sql']),
223+
object_name=command_obj.object_name,
224+
nsp_name=command_obj.nsp_name,
225+
pgadmin_alias=pgadmin_alias,
226+
primary_keys=primary_keys,
227+
has_oids=command_obj.has_oids()
228+
)
229+
list_of_sql[of_type].append({
230+
'sql': sql,
231+
'data': data,
232+
'client_row': each_row,
233+
'select_sql': select_sql,
234+
'row_id': data.get(client_primary_key)
235+
})
236+
else:
237+
list_of_sql[of_type].append({
238+
'sql': sql,
239+
'data': data,
240+
'row_id': data.get(client_primary_key)
241+
})
189242

190243
# For deleted rows
191244
elif of_type == 'deleted':
@@ -287,7 +340,7 @@ def failure_handle(res, row_id):
287340
if not status:
288341
return failure_handle(res, item.get('row_id', 0))
289342

290-
# Select added row from the table
343+
# Select added/updated row from the table
291344
if 'select_sql' in item:
292345
params = {
293346
pgadmin_alias[k] if k in pgadmin_alias else k: v

0 commit comments

Comments
 (0)