mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-11-01 07:48:03 -06:00
fix(sqleditor): skip read-only columns when saving inserted rows (#10015)
This commit is contained in:
1 parent
39684cdf48
commit
f9b9bc9d40
5 files changed
+108
-1
No files matched your search
@@ -35,3 +35,4 @@ Bug fixes
|
||||
| `Issue #9896 <https://github.com/pgadmin-org/pgadmin4/issues/9896>`_ - Fix invalid DDL reconstruction for SERIAL columns in Schema Diff and the generated SQL/CREATE Script so the output round-trips on a clean target.
|
||||
| `Issue #9935 <https://github.com/pgadmin-org/pgadmin4/issues/9935>`_ - Fix "Illegal instruction" crash on startup of the Linux DEB and RPM packages on older x86_64 CPUs by pinning the psycopg C extension build to the x86-64 baseline.
|
||||
| `Issue #9936 <https://github.com/pgadmin-org/pgadmin4/issues/9936>`_ - Fix the AI panel silently falling back to the default provider when a custom LLM API URL or key file was set, and allow self-hosted LLM endpoints on any loopback port.
|
||||
| `Issue #9939 <https://github.com/pgadmin-org/pgadmin4/issues/9939>`_ - Fix saving a newly-added row in the Query Tool failing when the result set includes expression or alias columns that are not real columns of the underlying table.
|
||||
@@ -1257,8 +1257,17 @@ export function ResultSet() {
|
||||
try {
|
||||
/* Convert the added info to actual rows */
|
||||
let added = {...dataChangeStore.added};
|
||||
/* Strip read-only columns (expression/alias columns in Query Tool)
|
||||
* from the new-row payload so the backend doesn't try to INSERT
|
||||
* them into the base table. Issue #9939. */
|
||||
let nonEditableKeys = new Set(
|
||||
columns.filter((c)=>c.can_edit === false).map((c)=>c.key)
|
||||
);
|
||||
Object.keys(added).forEach((clientPK)=>{
|
||||
added[clientPK].data = _.find(rows, (r)=>rowKeyGetter(r)==clientPK);
|
||||
let rowData = _.find(rows, (r)=>rowKeyGetter(r)==clientPK);
|
||||
added[clientPK].data = _.omitBy(
|
||||
rowData, (_v, k)=>nonEditableKeys.has(k)
|
||||
);
|
||||
});
|
||||
let {data: respData} = await rsu.current.saveData({
|
||||
updated: dataChangeStore.updated,
|
||||
|
||||
@@ -50,6 +50,11 @@ def get_columns_types(is_query_tool, columns_info, table_oid, conn, has_oids,
|
||||
col_type['type_name'] = None
|
||||
col_type['internal_size'] = col['internal_size']
|
||||
col_type['display_size'] = col['display_size']
|
||||
# Propagate per-column editability so the save path can refuse to
|
||||
# write expression/alias columns that don't exist in the base table.
|
||||
# View Data Tool columns are always real table columns: default True.
|
||||
col_type['is_editable'] = col.get('is_editable', True) \
|
||||
if is_query_tool else True
|
||||
column_types[col['name']] = col_type
|
||||
|
||||
if rset['rows']:
|
||||
|
||||
@@ -118,6 +118,18 @@ def save_changed_data(changed_data, columns_info, conn, command_obj,
|
||||
if command_obj.has_oids():
|
||||
data.pop('oid', None)
|
||||
|
||||
# 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`). Keys not
|
||||
# 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.
|
||||
data = {
|
||||
k: v for k, v in data.items()
|
||||
if k in columns_info and
|
||||
columns_info[k].get('is_editable', True)
|
||||
}
|
||||
|
||||
# Update columns value with columns having
|
||||
# not_null=False and has no default value
|
||||
column_data.update(data)
|
||||
|
||||
@@ -960,3 +960,83 @@ class TestSaveChangedData(BaseTestGenerator):
|
||||
url = '/sqleditor/close/{0}'.format(self.trans_id)
|
||||
response = self.tester.delete(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class TestSaveAddedRowSkipsNonEditableColumn(TestSaveChangedData):
|
||||
"""Regression test for issue #9939.
|
||||
|
||||
When a Query Tool result includes an expression or alias column
|
||||
(e.g. ``first_name || ' ' || last_name AS the_name``), the alias is
|
||||
not a real column of the underlying table. The new-row save flow
|
||||
must drop those keys before rendering INSERT; otherwise PostgreSQL
|
||||
rejects the row with ``column "the_name" does not exist``.
|
||||
"""
|
||||
|
||||
scenarios = [
|
||||
('Insert via SELECT that aliases a concatenation', dict(
|
||||
save_payload={
|
||||
"updated": {},
|
||||
"added": {
|
||||
"2": {
|
||||
"err": False,
|
||||
"data": {
|
||||
"id": "1",
|
||||
"__temp_PK": "2",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
# The client populates every column when
|
||||
# building a new row. ``the_name`` is the
|
||||
# aliased expression — sending it must not
|
||||
# break the INSERT.
|
||||
"the_name": None
|
||||
}
|
||||
}
|
||||
},
|
||||
"staged_rows": {},
|
||||
"deleted": {},
|
||||
"updated_index": {},
|
||||
"added_index": {"2": "2"},
|
||||
"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, "John", "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
|
||||
);
|
||||
""".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)
|
||||
Reference in new issue
Block a user