@@ -243,12 +243,13 @@ class CustomNames(_ColumnNames):
243243
244244
245245@pytest .mark .parametrize ("cycle" , ("plain" , "cursor" ))
246- def test_names_plan_gc_invalidation_completes_once (conn_str , cycle ):
246+ @pytest .mark .parametrize ("preparation" , ("names" , "converter" ))
247+ def test_plan_gc_invalidation_completes_once (conn_str , cycle , preparation ):
247248 _redesign_runtime ()
248- _redesign_subprocess ("_names_plan_gc_invalidation " , cycle )
249+ _redesign_subprocess ("_plan_gc_invalidation " , cycle , preparation )
249250
250251
251- def _names_plan_gc_invalidation (cycle ):
252+ def _plan_gc_invalidation (cycle , preparation ):
252253 import os
253254 from mssql_python .cursor import _NATIVE_FETCH_RUNTIME
254255
@@ -270,8 +271,27 @@ def _names_plan_gc_invalidation(cycle):
270271
271272 with _native_fetch_counters () as profiling , connection .cursor () as cursor :
272273 warm (cursor , "fetchone" )
273- names = tuple (cursor ._cached_result_columns )
274- cursor ._cached_result_columns = names
274+ calls = []
275+
276+ def convert (raw ):
277+ calls .append (raw )
278+ connection .remove_output_converter (mssql_python .SQL_INTEGER )
279+ return raw * 10
280+
281+ if preparation == "converter" :
282+ connection .add_output_converter (mssql_python .SQL_INTEGER , convert )
283+ assert cursor .fetchone ()[0 ] == 30
284+ assert calls == [3 ]
285+ with _fetchone_window (profiling ) as refresh :
286+ assert cursor .fetchone ()[0 ] == 4
287+ _assert_fetchone_counts (refresh , prepare = 0 , native = 0 , fallback = 1 , sql = 1 )
288+ assert cursor ._cached_converter_map == ()
289+ assert cursor ._cached_converters_generation == connection ._converters_generation
290+ expected = 5
291+ else :
292+ cursor ._cached_result_columns = tuple (cursor ._cached_result_columns )
293+ expected = 3
294+ names = cursor ._cached_result_columns
275295 collections = []
276296
277297 def collect_during_policy (frame , event , argument ):
@@ -290,16 +310,20 @@ def collect_during_policy(frame, event, argument):
290310 assert collections and collections [0 ] > 0
291311 if cycle == "cursor" :
292312 assert reference () is None
293- assert row [0 ] == 3 and row ._column_names is names
294- assert cursor .rowcount == 3 and cursor .rownumber == 2
313+ assert row [0 ] == expected and row ._column_names is names
314+ assert cursor .rowcount == expected and cursor .rownumber == expected - 1
295315 fallback = int (cycle == "cursor" )
296316 _assert_fetchone_counts (
297317 observation , prepare = 1 , native = 1 - fallback , fallback = fallback , sql = 1
298318 )
299- assert cursor .fetchone ()[0 ] == 4
300- assert cursor .fetchone ()[0 ] == 5
319+ assert observation ["python" ].get (convert .__code__ , 0 ) == 0
320+ if preparation == "converter" :
321+ assert calls == [3 ]
322+ for remaining in range (expected + 1 , 6 ):
323+ assert cursor .fetchone ()[0 ] == remaining
301324 assert cursor .fetchone () is None
302- assert row [0 ] == 3 and row ._column_names is names
325+ assert row [0 ] == expected and row ._column_names is names
326+ assert dict (row ._mapping ) == {"n" : expected }
303327
304328
305329@pytest .mark .parametrize ("sql_type" , ("CHAR" , "VARCHAR" , "NCHAR" , "NVARCHAR" ))
@@ -752,6 +776,9 @@ def convert(raw):
752776 _assert_fetchone_counts (refresh , prepare = 0 , native = 0 , fallback = 1 , sql = 1 )
753777 assert cursor ._cached_converter_map == ()
754778 assert cursor ._cached_converters_generation == connection ._converters_generation
779+ # Prior watched cursor cycles can invalidate a cold plan during GC.
780+ # Measure stable reenable here; the GC test covers that safe fallback.
781+ gc .collect ()
755782 with _fetchone_window (profiling , tracked ) as reenabled :
756783 assert value (read (cursor , method ), method ) == 5
757784 if tracked :
0 commit comments