Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -1309,8 +1309,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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ 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 complete row to get recalculated generated column values #}
{% if return_all_columns %} RETURNING *{% endif %};
10 changes: 10 additions & 0 deletions web/pgadmin/tools/sqleditor/utils/get_column_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,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']:
Expand All @@ -81,12 +86,17 @@ 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:
col_type['not_null'] = col['not_null'] = None
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
67 changes: 57 additions & 10 deletions web/pgadmin/tools/sqleditor/utils/save_changed_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,13 @@ def save_changed_data(changed_data, columns_info, conn, command_obj,
# known to the result set are dropped too. Without this
# guard the rendered INSERT references a non-existent
# column and Postgres rejects the row. Issue #9939.
# Also remove generated columns (GENERATED ALWAYS AS) as they
# cannot be inserted - PostgreSQL auto-computes their values.
data = {
k: v for k, v in data.items()
if k in columns_info and
columns_info[k].get('is_editable', True)
columns_info[k].get('is_editable', True) and
not columns_info[k].get('is_generated', False)
}

# Update columns value with columns having
Expand Down Expand Up @@ -175,14 +178,33 @@ 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 use
# RETURNING * to get recalculated values directly from UPDATE.
has_generated_cols = any(
col_info.get('is_generated', False)
for col_info in columns_info.values()
)

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.
data = {k: v for k, v in data.items()
if not columns_info.get(k, {}).get('is_generated',
False)}

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()
}

# Use RETURNING * when table has generated columns to get
# the complete updated row with recalculated values.
sql = render_template(
"/".join([command_obj.sql_path, 'update.sql']),
data_to_be_saved=data,
Expand All @@ -192,12 +214,26 @@ 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,
return_all_columns=has_generated_cols,
conn=conn
)
list_of_sql[of_type].append({'sql': sql,
'data': data,
'row_id':
data.get(client_primary_key)})

# For tables with generated columns, use 'returning_all'
# flag to indicate RETURNING * is used (no separate SELECT).
if has_generated_cols:
Comment thread
RohitBhati8269 marked this conversation as resolved.
list_of_sql[of_type].append({
'sql': sql,
'data': data,
'client_row': each_row,
'returning_all': True,
'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':
Expand Down Expand Up @@ -283,10 +319,16 @@ def failure_handle(res, row_id):
}

row_added = None
# Check if we need result data (INSERT with select_sql or
# UPDATE with RETURNING *)
needs_result = (
('select_sql' in item and item['select_sql']) or
item.get('returning_all', False)
)

try:
# Fetch oids/primary keys
if 'select_sql' in item and item['select_sql']:
# Fetch oids/primary keys or complete row
if needs_result:
status, res = conn.execute_dict(
item['sql'], item['data'])
else:
Expand All @@ -299,7 +341,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
# For INSERT: use RETURNING to get PKs, then SELECT full row
if 'select_sql' in item:
params = {
pgadmin_alias[k] if k in pgadmin_alias else k: v
Expand All @@ -314,6 +356,11 @@ def failure_handle(res, row_id):
if 'rows' in sel_res and len(sel_res['rows']) > 0:
row_added = {
item['client_row']: sel_res['rows'][0]}
# For UPDATE with RETURNING *: use result directly
elif item.get('returning_all', False):
if 'rows' in res and len(res['rows']) > 0:
row_added = {
item['client_row']: res['rows'][0]}

rows_affected = conn.rows_affected()
mogrified_sql = conn.mogrify(item['sql'], item['data'])
Expand Down
Loading