-
Notifications
You must be signed in to change notification settings - Fork 99
Description
Expected Behavior
When using PostgreSQL and calling an insert with RETURNING, Peewee’s documentation states that newly created primary keys should be populated for each inserted instance. (here)
For multi-row inserts (e.g. bulk operations), PostgreSQL returns one row per inserted record.
Example:
objs = [MyModel(name="a"), MyModel(name="b")]
res = await MyModel.insert_many(
[("a",), ("b",)],
fields=[MyModel.name]
).returning(MyModel.id).aio_execute()
# expected:
# [(1,), (2,)]Actual Behavior
peewee_async returns only the first row and also unwraps the tuple, producing only the scalar primary key of the first record.
This breaks any logic that relies on multi-row returning, including implementing an async version of Peewee’s bulk_create.
For instance, the original implementation:
class AioModelInsert(peewee.ModelInsert, AioQueryMixin):
async def fetch_results(self, cursor: CursorProtocol) -> Union[List[Any], Any, int]:
if self._returning is not None and len(self._returning) > 1:
return await fetch_models(cursor, self)
if self._returning:
row = await cursor.fetchone()
return row[0] if row else None
else:
return cursor.lastrowidProblems in this implementation:
1. fetchone() reads only a single row
PostgreSQL returns one row per inserted record, but fetchone() consumes only the first and discards the rest.
2. Returned tuple is unwrapped (row[0])
Peewee’s synchronous version returns tuples/lists, not raw values.
This produces an inconsistent output shape.
3. Bulk insert becomes impossible to use
Because only the first primary key is returned, and bulk operations need one primary key per instance to populate model objects.
Minimal Reproducible Example
res = await MyModel.insert_many(
[("a",), ("b",)],
fields=[MyModel.name]
).returning(MyModel.id).aio_execute()
print(res)Expected:
[(1,), (2,)]
Actual (peewee_async):
1
(only the first id, unwrapped from its tuple)
Cause of the Bug
The logic handling the RETURNING clause is inconsistent with Peewee’s ModelInsert:
- multi-row insert → should read all rows
- single-field returning → should still return list of tuples
fetchone()androw[0]deny both invariants
Proposed Fix
This corrected version aligns with Peewee’s synchronous behavior and correctly handles multi-row returning:
class AioModelInsert(peewee.ModelInsert, AioQueryMixin):
async def fetch_results(self, cursor: CursorProtocol) -> Union[List[Any], Any, int]:
if isinstance(self._insert,dict):
row = await cursor.fetchone()
return row[0] if row else None
elif self._as_rowcount:
return cursor.lastrowid
else:
return await fetch_models(cursor, self)Additional Notes
With this fix, multi-row inserts with RETURNING work correctly, enabling functionality equivalent to Peewee’s bulk_create in async workflows without patching the library externally.