5151 GC_HOLD_STAGING ,
5252 GcJobReport ,
5353)
54+ from tests ._sqlite_single_step import SingleStepConnection
5455from tests .test_canonical_roundtrip import fixture_model
5556
5657_NS = "lineage-gc"
@@ -471,6 +472,49 @@ def _page_stats(store: RunStore) -> tuple[int, int, int]:
471472 )
472473
473474
475+ def _store_of_twelve_runs (tmp_path : Path ) -> RunStore :
476+ """Twelve published runs of growing width: a sweep that keeps one frees
477+ pages worth measuring, on a store opened with incremental auto-vacuum."""
478+
479+ store = _store (tmp_path )
480+ for index in range (12 ):
481+ _publish (
482+ store ,
483+ _wider_model (* (f"pad-{ index } -{ n } " for n in range (400 + index ))),
484+ expected_generation = index ,
485+ )
486+ return store
487+
488+
489+ def test_the_release_drains_the_freelist_when_the_cursor_steps_the_pragma_once (
490+ tmp_path : Path ,
491+ ) -> None :
492+ """The release must not depend on the cursor stepping the pragma to its end.
493+
494+ CPython 3.11's ``sqlite3`` resets a statement that reports no result
495+ columns after its first step, so ``execute("PRAGMA
496+ incremental_vacuum").fetchall()`` freed one page per call there: the
497+ 3.11 job left 228 pages of this store on the freelist (CI, 2026-09-27)
498+ while 3.10 and 3.12+ were green. ``SingleStepConnection`` reproduces that
499+ stepping on every interpreter, and the sweep's release still has to leave
500+ the freelist empty through it.
501+ """
502+
503+ store = _store_of_twelve_runs (tmp_path )
504+ store ._connection = SingleStepConnection (store ._connection ) # type: ignore[assignment]
505+ _ , pages_before , freelist_before = _page_stats (store )
506+ assert freelist_before == 0
507+
508+ collect_garbage (store , retain_history = 1 )
509+
510+ _ , pages_after , freelist_after = _page_stats (store )
511+ assert pages_after < pages_before , "the sweep freed nothing, so nothing is proved"
512+ assert freelist_after == 0 , (
513+ f"{ freelist_after } freed pages were left on the freelist: the release "
514+ "stepped the pragma once instead of draining it"
515+ )
516+
517+
474518def test_the_sweep_returns_the_pages_it_freed_to_the_file (tmp_path : Path ) -> None :
475519 """A collected run must leave the FILE, not only the tables.
476520
@@ -490,13 +534,7 @@ def test_the_sweep_returns_the_pages_it_freed_to_the_file(tmp_path: Path) -> Non
490534 sweep reports about itself.
491535 """
492536
493- store = _store (tmp_path )
494- for index in range (12 ):
495- _publish (
496- store ,
497- _wider_model (* (f"pad-{ index } -{ n } " for n in range (400 + index ))),
498- expected_generation = index ,
499- )
537+ store = _store_of_twelve_runs (tmp_path )
500538
501539 auto_vacuum , pages_before , freelist_before = _page_stats (store )
502540 assert auto_vacuum == 2 , (
0 commit comments