Skip to content

Commit 2dabb8d

Browse files
jahnvi480Copilot
andcommitted
FIX: isolate converter reenable counts from prior cursor GC
Collect prior watched cursor cycles before asserting stable native reenable counts. Extend deterministic GC coverage to converter callback removal, preserving exact fetch/fallback counts, metadata ownership, and callback semantics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 32d222b commit 2dabb8d

1 file changed

Lines changed: 37 additions & 10 deletions

File tree

‎tests/test_native_fetch_dispatch.py‎

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)