@@ -195,6 +195,22 @@ def fetchall(self):
195195 def close (self ):
196196 pass
197197
198+ def __iter__ (self ):
199+ """Support direct cursor iteration (for row in cursor).
200+
201+ This is required by Django's django.contrib.postgres which iterates
202+ over cursor results to register type handlers (hstore, citext, etc.).
203+ """
204+ return self
205+
206+ def __next__ (self ):
207+ """Return next row for iteration."""
208+ if self ._mock_index >= len (self ._mock_rows ):
209+ raise StopIteration
210+ row = self ._mock_rows [self ._mock_index ]
211+ self ._mock_index += 1
212+ return tuple (row ) if isinstance (row , list ) else row
213+
198214 def __enter__ (self ):
199215 return self
200216
@@ -486,6 +502,30 @@ def executemany(self, query: QueryType, vars_list: Any) -> Any:
486502 logger .debug ("[INSTRUMENTED_CURSOR] executemany() called on instrumented cursor" )
487503 return instrumentation ._traced_executemany (self , super ().executemany , sdk , query , vars_list )
488504
505+ def __iter__ (self ):
506+ """Support direct cursor iteration (for row in cursor).
507+
508+ If _tusk_rows is set (from _finalize_query_span recording), use it.
509+ Otherwise fall back to the base cursor's iteration.
510+ """
511+ if hasattr (self , "_tusk_rows" ):
512+ return self
513+ return super ().__iter__ ()
514+
515+ def __next__ (self ):
516+ """Return next row for iteration.
517+
518+ If _tusk_rows is set (from _finalize_query_span recording), iterate over stored rows.
519+ Otherwise fall back to the base cursor's __next__.
520+ """
521+ if hasattr (self , "_tusk_rows" ):
522+ if self ._tusk_index < len (self ._tusk_rows ): # pyright: ignore[reportAttributeAccessIssue]
523+ row = self ._tusk_rows [self ._tusk_index ] # pyright: ignore[reportAttributeAccessIssue]
524+ self ._tusk_index += 1 # pyright: ignore[reportAttributeAccessIssue]
525+ return row
526+ raise StopIteration
527+ return super ().__next__ ()
528+
489529 return InstrumentedCursor
490530
491531 def _traced_execute (
@@ -1014,6 +1054,8 @@ def patched_fetchall():
10141054 cursor .fetchone = patched_fetchone # pyright: ignore[reportAttributeAccessIssue]
10151055 cursor .fetchmany = patched_fetchmany # pyright: ignore[reportAttributeAccessIssue]
10161056 cursor .fetchall = patched_fetchall # pyright: ignore[reportAttributeAccessIssue]
1057+ # Note: __iter__ and __next__ are handled at class level in InstrumentedCursor
1058+ # (instance-level dunder patching doesn't work for C extension cursors)
10171059
10181060 except Exception as fetch_error :
10191061 logger .debug (f"Could not fetch rows (query might not return rows): { fetch_error } " )
0 commit comments