3535_FETCHONE_BUFFER_MAX_ROWS = 1000
3636
3737# After a fetchone() batch conversion fails, the batch is retried one row at a time.
38- # If this many leading rows all fail too, the failure is treated as systematic: the
39- # rest of the batch is not retried row by row (each would cost a separate
40- # conversion) and those rows re-raise the original batch error instead.
38+ # If this many leading rows all fail too, with the same error type and message as
39+ # the batch, the failure is treated as systematic: the rest of the batch is not
40+ # retried row by row (each would cost a separate conversion) and those rows re-raise
41+ # the original batch error instead. Leading rows failing for other reasons do not
42+ # trigger the shortcut, so later good rows are still converted.
4143_FETCHONE_FALLBACK_PROBE_ROWS = 5
4244
4345
4446class _FailedRow :
4547 """Placeholder in the fetchone() row buffer for a row that failed to convert."""
4648
47- __slots__ = ("error" ,)
49+ __slots__ = ("error" , "traceback" )
4850
4951 def __init__ (self , error : Exception ):
5052 self .error = error
53+ # Snapshot the conversion traceback. Systematic failures park the same
54+ # exception instance in many rows; restoring this before each raise keeps
55+ # its __traceback__ from growing (and pinning frames) on every fetchone().
56+ self .traceback = error .__traceback__
5157
5258
5359class ResultSet (ABC ):
@@ -200,7 +206,7 @@ def _row_buffer_take_rows(self, n: Optional[int] = None) -> List[Row]:
200206 if isinstance (row , _FailedRow ):
201207 # The taken rows are consumed either way, matching how a failed
202208 # conversion behaved before fetchone() buffering.
203- raise row .error
209+ raise row .error . with_traceback ( row . traceback )
204210 return taken
205211
206212 @staticmethod
@@ -219,20 +225,29 @@ def _convert_rows_individually(
219225
220226 Rows that fail are returned as ``_FailedRow`` placeholders so the error is
221227 raised only when that row is handed out. If the first
222- ``_FETCHONE_FALLBACK_PROBE_ROWS`` rows all fail, the failure is treated as
223- systematic and the remaining rows are parked with ``batch_error`` instead of
224- being converted one by one. Returns None if ``raw`` has no rows to split, in
228+ ``_FETCHONE_FALLBACK_PROBE_ROWS`` rows all fail with the same error as the
229+ batch (same type and message), the failure is treated as systematic and the
230+ remaining rows are parked with ``batch_error`` instead of being converted one
231+ by one. Otherwise every row is tried individually. Returns None if ``raw`` has no rows to split, in
225232 which case the caller re-raises the batch error.
226233 """
227234 n = ResultSet ._raw_row_count (raw )
228235 if not n :
229236 return None
237+ def same_as_batch (r : Any ) -> bool :
238+ return (
239+ isinstance (r , _FailedRow )
240+ and type (r .error ) is type (batch_error )
241+ and str (r .error ) == str (batch_error )
242+ )
243+
230244 rows : List [Any ] = []
231245 systematic = False
232246 for i in range (n ):
233- if i == _FETCHONE_FALLBACK_PROBE_ROWS and all (
234- isinstance (r , _FailedRow ) for r in rows
235- ):
247+ # Only skip the remaining per-row retries when every probe row failed
248+ # with the same error as the batch; leading rows that fail for their
249+ # own reasons must not cause later good rows to be dropped.
250+ if i == _FETCHONE_FALLBACK_PROBE_ROWS and all (same_as_batch (r ) for r in rows ):
236251 systematic = True
237252 rows .extend (_FailedRow (batch_error ) for _ in range (n - i ))
238253 break
@@ -242,16 +257,20 @@ def _convert_rows_individually(
242257 except Exception as e :
243258 rows .append (_FailedRow (e ))
244259 failed = sum (isinstance (r , _FailedRow ) for r in rows )
260+ # One summary line, no traceback: the error itself still reaches the caller
261+ # when the failing row is fetched. The full traceback is only logged at DEBUG.
245262 logger .warning (
246263 "fetchone(): failed to convert %d of %d row(s) in a batch%s; each failing "
247- "row raises when fetched" ,
264+ "row raises when fetched (%s: %s) " ,
248265 failed ,
249266 n ,
250267 " (first %d rows all failed, rest not retried)" % _FETCHONE_FALLBACK_PROBE_ROWS
251268 if systematic
252269 else "" ,
253- exc_info = batch_error ,
270+ type (batch_error ).__name__ ,
271+ batch_error ,
254272 )
273+ logger .debug ("fetchone(): batch conversion error" , exc_info = batch_error )
255274 return rows
256275
257276 def _cap_to_current_chunk (self , size : int ) -> int :
0 commit comments