You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
updateMany / deleteMany return only the first column of a composite primary key (e.g. "1") instead of the pipe-joined ID used everywhere else (e.g. "1|2").
This is a regression/gap in the compound-key support from #769, not a new feature request. Single-record endpoints already implement "col1|col2" correctly. Bulk endpoints do not.
Expected
react-admin requires updateMany / deleteMany to return the same record IDs used by getList / getOne / getMany:
{"data": ["1|2", "1|3"]}
That format is the contract from #769 and from _convert_record():
Against a table with PK (id, other) and rows (1, 2), (1, 3), (4, 5):
Endpoint
Status
IDs
GET …/list
200
["1|2", "1|3", "4|5"]
GET …/one?id=1|2
200
"1|2"
GET …/many?ids=["1|2","1|3"]
200
"1|2", "1|3"
PUT …/update_many?ids=["1|2","1|3"]
200
["1", "1"]
DELETE …?ids=["4|5"]
200
["4"]
The bulk writes succeed (incoming "1|2" is parsed correctly and the rows are updated/deleted). Only the response IDs are wrong. Two different rows both come back as "1", so react-admin’s cache / selection / unselect after a bulk action will desync.
Why this is a bug, not intentional
Compound primary keys #769 defined "first|second" as the ID format for compound keys and was closed as implemented.
List/get-one/get-many already return that format; bulk is inconsistent with the same resource.
_convert_ids() is documented as converting IDs to the “correct output format”, but it does str(i) instead of the same "|".join(...) as _convert_record().
SAResource.update_many / delete_many are typed as returning list[tuple[Any, ...]] (full PK tuples) but call Result.scalars(), which only yields the first RETURNING column.
Root cause
1. SQLAlchemy backend drops extra PK columns (aiohttp_admin/backends/sqlalchemy.py):
stmt=stmt.values(data).returning(*(self._table.c[pk] forpkinself.primary_key))
returnlist(awaitconn.scalars(stmt)) # first column only
Same pattern in delete_many.
2. ID encoding does not pipe-join (aiohttp_admin/backends/abc.py):
def_convert_ids(self, ids: Sequence[_ID]) ->tuple[str, ...]:
returntuple(str(i) foriinids) # "1" or "(1, 2)", never "1|2"
Even if (1) returned full tuples, str((1, 2)) would be "(1, 2)", not "1|2".
Summary
updateMany/deleteManyreturn only the first column of a composite primary key (e.g."1") instead of the pipe-joined ID used everywhere else (e.g."1|2").This is a regression/gap in the compound-key support from #769, not a new feature request. Single-record endpoints already implement
"col1|col2"correctly. Bulk endpoints do not.Expected
react-admin requires
updateMany/deleteManyto return the same record IDs used bygetList/getOne/getMany:{"data": ["1|2", "1|3"]}That format is the contract from #769 and from
_convert_record():Actual (reproduced on current
master)Against a table with PK
(id, other)and rows(1, 2),(1, 3),(4, 5):GET …/list["1|2", "1|3", "4|5"]GET …/one?id=1|2"1|2"GET …/many?ids=["1|2","1|3"]"1|2","1|3"PUT …/update_many?ids=["1|2","1|3"]["1", "1"]DELETE …?ids=["4|5"]["4"]The bulk writes succeed (incoming
"1|2"is parsed correctly and the rows are updated/deleted). Only the response IDs are wrong. Two different rows both come back as"1", so react-admin’s cache / selection / unselect after a bulk action will desync.Why this is a bug, not intentional
"first|second"as the ID format for compound keys and was closed as implemented._convert_ids()is documented as converting IDs to the “correct output format”, but it doesstr(i)instead of the same"|".join(...)as_convert_record().SAResource.update_many/delete_manyare typed as returninglist[tuple[Any, ...]](full PK tuples) but callResult.scalars(), which only yields the first RETURNING column.Root cause
1. SQLAlchemy backend drops extra PK columns (
aiohttp_admin/backends/sqlalchemy.py):Same pattern in
delete_many.2. ID encoding does not pipe-join (
aiohttp_admin/backends/abc.py):Even if (1) returned full tuples,
str((1, 2))would be"(1, 2)", not"1|2".Test gap (no existing coverage)
tests/test_backends_sqlalchemy.py::test_id_nonpkprimary_key == ("id", "other")tests/test_backends_sqlalchemy.py::test_nonid_pk_apiidPKtests/test_views.py::test_update_many/test_delete_manyDummy2Modelinteger PK, expects{"data": ["1", "2"]}admin-js/tests/relationships.test.jsThat is why this slipped through: compound PK support was implemented and tested for get/create/update-one, never for bulk.
Suggested fix
RETURNING([tuple(row) for row in await conn.execute(stmt)]) instead ofscalars()."|".join(str(part) for part in i).update_many/delete_manyasserting{"data": ["1|2", "1|3"]}.Single-column PKs stay
"1"/"2"because(1,)joins to"1".