diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/12_plus/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/12_plus/nodes.sql new file mode 100644 index 00000000000..5b6d100bc63 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/templates/columns/sql/12_plus/nodes.sql @@ -0,0 +1,40 @@ +SELECT DISTINCT att.attname as name, att.attnum as OID, pg_catalog.format_type(ty.oid,NULL) AS datatype, +pg_catalog.format_type(ty.oid,att.atttypmod) AS displaytypname, +att.attnotnull as not_null, +CASE WHEN att.atthasdef OR att.attidentity != '' OR ty.typdefault IS NOT NULL THEN True +ELSE False END as has_default_val, des.description, seq.seqtypid, +{# Detect generated columns to exclude from INSERT/UPDATE in View/Edit Data #} +CASE WHEN att.attgenerated = 's' THEN true ELSE false END as is_generated +FROM pg_catalog.pg_attribute att + JOIN pg_catalog.pg_type ty ON ty.oid=atttypid + JOIN pg_catalog.pg_namespace tn ON tn.oid=ty.typnamespace + JOIN pg_catalog.pg_class cl ON cl.oid=att.attrelid + JOIN pg_catalog.pg_namespace na ON na.oid=cl.relnamespace + LEFT OUTER JOIN pg_catalog.pg_type et ON et.oid=ty.typelem + LEFT OUTER JOIN pg_catalog.pg_attrdef def ON adrelid=att.attrelid AND adnum=att.attnum + 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 + LEFT OUTER JOIN pg_catalog.pg_namespace ns ON ns.oid=cs.relnamespace + LEFT OUTER JOIN pg_catalog.pg_index pi ON pi.indrelid=att.attrelid AND indisprimary + LEFT OUTER JOIN pg_catalog.pg_description des ON (des.objoid=att.attrelid AND des.objsubid=att.attnum AND des.classoid='pg_class'::regclass) + LEFT OUTER JOIN pg_catalog.pg_sequence seq ON cs.oid=seq.seqrelid +WHERE + +{% if tid %} + att.attrelid = {{ tid|qtLiteral(conn) }}::oid +{% endif %} +{% if table_name and table_nspname %} + cl.relname= {{table_name |qtLiteral(conn)}} and na.nspname={{table_nspname|qtLiteral(conn)}} +{% endif %} +{% if clid %} + AND att.attnum = {{ clid|qtLiteral(conn) }} +{% endif %} +{### To show system objects ###} +{% if not show_sys_objects and not has_oids %} + AND att.attnum > 0 +{% endif %} +{### To show oids in view data ###} +{% if has_oids %} + AND (att.attnum > 0 OR (att.attname = 'oid' AND att.attnum < 0)) +{% endif %} + AND att.attisdropped IS FALSE +ORDER BY att.attnum diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx index 076ca84c717..172ad674659 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx @@ -1300,8 +1300,10 @@ export function ResultSet() { } pageDataOutOfSync.current = true; - if(_.size(dataChangeStore.added)) { - // Update the rows in a grid after addition + // Update the rows in a grid after addition/update. + // row_added contains refetched row data with recalculated + // generated column values (for both INSERT and UPDATE). + if(_.size(dataChangeStore.added) || _.size(dataChangeStore.updated)) { respData.data.query_results.forEach((qr)=>{ if(!_.isNull(qr.row_added)) { let rowClientPK = Object.keys(qr.row_added)[0]; diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/update.sql b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/update.sql index 08ac1c5f428..ee8ebb0a9ec 100644 --- a/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/update.sql +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/sql/default/update.sql @@ -4,4 +4,7 @@ UPDATE {{ conn|qtIdent(nsp_name, object_name) | replace("%", "%%") }} SET {% if not loop.first %}, {% endif %}{{ conn|qtIdent(col) | replace("%", "%%") }} = %({{ pgadmin_alias[col] }})s{% if type_cast_required[col] %}::{{ data_type[col] }}{% endif %}{% endfor %} WHERE {% for pk in primary_keys %} -{% if not loop.first %} AND {% endif %}{{ conn|qtIdent(pk) | replace("%", "%%") }} = {{ primary_keys[pk]|qtLiteral(conn) }}{% endfor %}; +{% if not loop.first %} AND {% endif %}{{ conn|qtIdent(pk) | replace("%", "%%") }} = {{ primary_keys[pk]|qtLiteral(conn) }}{% endfor %} +{# Return primary keys to refetch row with recalculated generated column values #} +{% if pk_names and not has_oids %} RETURNING {{pk_names | replace("%", "%%")}}{% endif %} +{% if has_oids %} RETURNING oid{% endif %}; diff --git a/web/pgadmin/tools/sqleditor/utils/get_column_types.py b/web/pgadmin/tools/sqleditor/utils/get_column_types.py index 251ff422560..f5e53676b16 100644 --- a/web/pgadmin/tools/sqleditor/utils/get_column_types.py +++ b/web/pgadmin/tools/sqleditor/utils/get_column_types.py @@ -64,6 +64,11 @@ def get_columns_types(is_query_tool, columns_info, table_oid, conn, has_oids, col_type['seqtypid'] = col['seqtypid'] = \ rset['rows'][key]['seqtypid'] + # Check if column is a generated column (PostgreSQL 12+). + # Generated columns must be excluded from INSERT/UPDATE. + col_type['is_generated'] = col['is_generated'] = \ + rset['rows'][key].get('is_generated', False) + else: for row in rset['rows']: if row['oid'] == col['table_column']: @@ -76,6 +81,10 @@ def get_columns_types(is_query_tool, columns_info, table_oid, conn, has_oids, col_type['seqtypid'] = col['seqtypid'] = \ row['seqtypid'] + + # Check if column is a generated column (PG 12+). + col_type['is_generated'] = col['is_generated'] = \ + row.get('is_generated', False) break else: @@ -83,5 +92,6 @@ def get_columns_types(is_query_tool, columns_info, table_oid, conn, has_oids, col_type['has_default_val'] = \ col['has_default_val'] = None col_type['seqtypid'] = col['seqtypid'] = None + col_type['is_generated'] = col['is_generated'] = False return column_types diff --git a/web/pgadmin/tools/sqleditor/utils/save_changed_data.py b/web/pgadmin/tools/sqleditor/utils/save_changed_data.py index e2c521bc257..c68ad5542b4 100644 --- a/web/pgadmin/tools/sqleditor/utils/save_changed_data.py +++ b/web/pgadmin/tools/sqleditor/utils/save_changed_data.py @@ -118,6 +118,12 @@ def save_changed_data(changed_data, columns_info, conn, command_obj, if command_obj.has_oids(): data.pop('oid', None) + # Remove generated columns (GENERATED ALWAYS AS) as they + # cannot be inserted - PostgreSQL auto-computes their values. + for col_name, col_info in columns_info.items(): + if col_info.get('is_generated', False): + data.pop(col_name, None) + # Update columns value with columns having # not_null=False and has no default value column_data.update(data) @@ -163,14 +169,38 @@ def save_changed_data(changed_data, columns_info, conn, command_obj, # For updated rows elif of_type == 'updated': list_of_sql[of_type] = [] + + # Check if table has generated columns. If yes, we need to + # refetch row after UPDATE to get recalculated values for UI. + has_generated_cols = any( + col_info.get('is_generated', False) + for col_info in columns_info.values() + ) + + # Get primary keys info (same as INSERT) - needed for RETURNING + # clause and SELECT query to refetch updated row. + pk_names, primary_keys = command_obj.get_primary_keys() + for each_row in changed_data[of_type]: data = changed_data[of_type][each_row]['data'] + row_primary_keys = changed_data[of_type][each_row][ + 'primary_keys'] + + # Remove generated columns (GENERATED ALWAYS AS) as they + # cannot be updated - PostgreSQL auto-computes their values. + for col_name, col_info in columns_info.items(): + if col_info.get('is_generated', False): + data.pop(col_name, None) + pk_escaped = { pk: pk_val.replace('%', '%%') if hasattr( pk_val, 'replace') else pk_val - for pk, pk_val in - changed_data[of_type][each_row]['primary_keys'].items() + for pk, pk_val in row_primary_keys.items() } + + # Pass pk_names and has_oids for RETURNING clause in + # UPDATE statement. + # This will help to fetch the updated row's. sql = render_template( "/".join([command_obj.sql_path, 'update.sql']), data_to_be_saved=data, @@ -180,12 +210,35 @@ def save_changed_data(changed_data, columns_info, conn, command_obj, nsp_name=command_obj.nsp_name, data_type=column_type, type_cast_required=type_cast_required, + pk_names=pk_names if has_generated_cols else None, + has_oids=command_obj.has_oids(), conn=conn ) - list_of_sql[of_type].append({'sql': sql, - 'data': data, - 'row_id': - data.get(client_primary_key)}) + + # For tables with generated columns, add select_sql to + # refetch updated row. + if has_generated_cols: + select_sql = render_template( + "/".join([command_obj.sql_path, 'select.sql']), + object_name=command_obj.object_name, + nsp_name=command_obj.nsp_name, + pgadmin_alias=pgadmin_alias, + primary_keys=primary_keys, + has_oids=command_obj.has_oids() + ) + list_of_sql[of_type].append({ + 'sql': sql, + 'data': data, + 'client_row': each_row, + 'select_sql': select_sql, + 'row_id': data.get(client_primary_key) + }) + else: + list_of_sql[of_type].append({ + 'sql': sql, + 'data': data, + 'row_id': data.get(client_primary_key) + }) # For deleted rows elif of_type == 'deleted': @@ -287,7 +340,7 @@ def failure_handle(res, row_id): if not status: return failure_handle(res, item.get('row_id', 0)) - # Select added row from the table + # Select added/updated row from the table if 'select_sql' in item: params = { pgadmin_alias[k] if k in pgadmin_alias else k: v