Skip to content
This repository was archived by the owner on Aug 19, 2025. It is now read-only.

Commit 128bd60

Browse files
Merge pull request #111 from encode/sm-Fifteen-master
Iterating outside of transaction
2 parents ea8d93b + 5bcca88 commit 128bd60

File tree

2 files changed

+63
-7
lines changed

2 files changed

+63
-7
lines changed

databases/core.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,12 @@ async def execute_many(
219219
async def iterate(
220220
self, query: typing.Union[ClauseElement, str], values: dict = None
221221
) -> typing.AsyncGenerator[typing.Any, None]:
222-
async with self._query_lock:
223-
async for record in self._connection.iterate(
224-
self._build_query(query, values)
225-
):
226-
yield record
222+
async with self.transaction():
223+
async with self._query_lock:
224+
async for record in self._connection.iterate(
225+
self._build_query(query, values)
226+
):
227+
yield record
227228

228229
def transaction(self, *, force_rollback: bool = False) -> "Transaction":
229230
return Transaction(self, force_rollback)

tests/test_databases.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -704,10 +704,65 @@ async def test_database_url_interface(database_url):
704704
@pytest.mark.parametrize("database_url", DATABASE_URLS)
705705
@async_adapter
706706
async def test_concurrent_access_on_single_connection(database_url):
707+
database_url = DatabaseURL(database_url)
708+
if database_url.dialect != "postgresql":
709+
pytest.skip("Test requires `pg_sleep()`")
710+
707711
async with Database(database_url, force_rollback=True) as database:
708712

709713
async def db_lookup():
710-
if str(database_url).startswith("postgresql"):
711-
await database.fetch_one("SELECT pg_sleep(1)")
714+
await database.fetch_one("SELECT pg_sleep(1)")
712715

713716
await asyncio.gather(db_lookup(), db_lookup())
717+
718+
719+
@pytest.mark.parametrize("database_url", DATABASE_URLS)
720+
@async_adapter
721+
async def test_iterate_outside_transaction_with_values(database_url):
722+
"""
723+
Ensure `iterate()` works even without a transaction on all drivers.
724+
The asyncpg driver relies on server-side cursors without hold
725+
for iteration, which requires a transaction to be created.
726+
This is mentionned in both their documentation and their test suite.
727+
"""
728+
729+
database_url = DatabaseURL(database_url)
730+
if database_url.dialect == "mysql":
731+
pytest.skip("MySQL does not support `FROM (VALUES ...)` (F641)")
732+
733+
async with Database(database_url) as database:
734+
query = "SELECT * FROM (VALUES (1), (2), (3), (4), (5)) as t"
735+
iterate_results = []
736+
737+
async for result in database.iterate(query=query):
738+
iterate_results.append(result)
739+
740+
assert len(iterate_results) == 5
741+
742+
743+
@pytest.mark.parametrize("database_url", DATABASE_URLS)
744+
@async_adapter
745+
async def test_iterate_outside_transaction_with_temp_table(database_url):
746+
"""
747+
Same as test_iterate_outside_transaction_with_values but uses a
748+
temporary table instead of a list of values.
749+
"""
750+
751+
database_url = DatabaseURL(database_url)
752+
if database_url.dialect == "sqlite":
753+
pytest.skip("SQLite interface does not work with temporary tables.")
754+
755+
async with Database(database_url) as database:
756+
query = "CREATE TEMPORARY TABLE no_transac(num INTEGER)"
757+
await database.execute(query)
758+
759+
query = "INSERT INTO no_transac(num) VALUES (1), (2), (3), (4), (5)"
760+
await database.execute(query)
761+
762+
query = "SELECT * FROM no_transac"
763+
iterate_results = []
764+
765+
async for result in database.iterate(query=query):
766+
iterate_results.append(result)
767+
768+
assert len(iterate_results) == 5

0 commit comments

Comments
 (0)