Repository navigation
fix(spatial): enumerate R-tree pages so spatial indexes are freed and rebuilt (#189) - #205
Conversation
… rebuilt (EntglDb#189) RTreeIndex had no CollectAllPages, so CollectionSecondaryIndex.CollectAllPages returned nothing for a spatial index and FreeAllPages was a no-op. As a result VacuumAsync(RebuildIndexes) skipped spatial indexes, dropping a spatial index or its collection leaked every page of the tree, and a failed rebuild would have leaked its half-built copy. Add RTreeIndex.CollectAllPages() and CollectAllPages(ulong transactionId): walk from the root, read each node as the given transaction sees it, and follow the child pointers of non-leaf entries. The transaction overload matters because SplitNode allocates pages outside the transaction and links them only from nodes written in it, so after a rollback they cannot be found from the committed tree. Wire it into CollectionSecondaryIndex.CollectAllPages, the drop-collection path in StorageEngine.FreeCollectionPagesCore, and remove the spatial skip in DocumentCollection.VacuumAsync. Tests: RTreeIndexPagesTests (empty tree, split tree, transaction view before and after rollback/commit, drop-collection frees spatial pages) and two VacuumTests replacing Spatial_IsKept (rebuild frees every old page; failed rebuild keeps the old tree and frees the copy). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mrdevrobot
left a comment
There was a problem hiding this comment.
Thanks. CollectAllPages walks every non-leaf entry from the fixed root, including nodes emptied by #172's Delete, the seen set prevents double counting, and the transactional overload is what RebuildIndexFromScratchAsync's failure path needs. Probe on the merged state: 2,000 points, half deleted, 5 rounds of VacuumAsync(RebuildIndexes = true) with a checkpoint after each: the file stayed at 239 pages, the spatial tree went from 16 to 5 pages and stayed there, and Within/Near returned identical results every round. Dropping the spatial index and creating a B-Tree reused 5 of its pages with no corruption, also after reopening. Both new VacuumTests fail on main; full suite merged 2463 / 0 / 4 skipped.
Approving. One fix: src/BLite.Core/VacuumOptions.cs:44 still says "Spatial indexes are not rebuilt." Please remove or update it.
Nits: a remark in the CollectAllPages doc that callers hold the collection lock; a debug assert when seen hides a cycle. DynamicCollection not freeing old index pages on rebuild and drop is now #211.
…ckfill Conflict in DocumentCollection.VacuumAsync: main (EntglDb#205) removed the spatial skip now that R-trees enumerate their pages; this branch passes the index object to RebuildIndexFromScratchAsync. Kept both: every index, spatial included, is rebuilt through RebuildIndexFromScratchAsync(index). Also drops the CreateUnregistered remark that a spatial index cannot enumerate its pages, which EntglDb#205 made obsolete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #189.
What was wrong
RTreeIndexcould not enumerate its pages.CollectionSecondaryIndex.CollectAllPagesreturned an empty list for spatial indexes andFreeAllPagesdid nothing, so:VacuumAsync(RebuildIndexes = true)skipped spatial indexes (if (index.Definition.Type == IndexType.Spatial) continue;).DropIndex→FreeAllPagesno-op;FreeCollectionPagesCoreskippedIndexType.Spatialwith a comment that a later VACUUM would reclaim the pages, which nothing does).Change
RTreeIndex.CollectAllPages()/CollectAllPages(ulong transactionId): the same walk asSearchwithout the bounding-box filter. Starts at_rootPageId, reads each node as the given transaction sees it (transactionId == 0→ committed state), pushespointer.PageIdfor every entry of a non-leaf node. A visited set guards against a corrupt parent pointer looping the walk. The transaction overload is needed becauseSplitNodeallocates its pages outside the transaction and links them only from nodes written in it, so after a rollback they are unreachable from the committed tree.RebuildIndexFromScratchAsyncalready collects the copy's pages through the transaction before rolling back; it now works for spatial copies too.CollectionSecondaryIndex.CollectAllPages: returns the R-tree's pages for a spatial index, soFreeAllPages,DropIndexandReplaceIndexfree them.StorageEngine.FreeCollectionPagesCore: theIndexType.Spatialcase builds anRTreeIndexon the stored root and collects its pages, like the B-tree and vector cases.DocumentCollection.VacuumAsync: the spatial skip is removed. Spatial indexes go through the sameCreateUnregisteredCopy→ fill → commit →ReplaceIndexpath as the other kinds (fix(vacuum): rebuild typed-collection indexes into fresh trees (#167) #170).Tests
RTreeIndexPagesTests(new):FreeCollectionPagesCore("geo_items")on a 3000-point collection returns every page ofidx_spatial.VacuumTests:Vacuum_RebuildIndexes_DocumentCollection_Spatial_IsKeptis replaced by..._Spatial_RebuildsTreeAndFreesOldPages: 3000 points, rebuild → new root persisted and in use,Withinstill returns the expected 100 points, and every page of the old tree (collected before the vacuum) is back on the free list or queued for the next WAL truncation (AssertPagesFreedAsync, which checks the exact page set rather than a count);..._Spatial_FailedRebuild_KeepsIndexAndFreesCopy: 6000 points, last document corrupted → the vacuum throws, the stored and in-memory root are unchanged,Withinstill works, and the copy's pages are freed (AssertCopyWasFreedAsync).CorruptLastDocumentAsyncwas generalised over the collection type for this.Mutation check: with
RTreeIndex.CollectAllPagespresent but the three wiring edits reverted, the rebuild, failed-rebuild and drop-collection tests fail and the four tree-level tests pass. Full suite: 2416 passed, 4 skipped.Not covered here
DynamicCollection.VacuumAsync(RebuildIndexes)builds fresh trees for every index kind but frees none of the old ones, andDynamicCollection.DropIndexremoves the entry without freeing any pages, for B-tree and vector indexes as well as spatial. That is a separate change on the dynamic side; this PR gives it the R-tree walk it would need.🤖 Generated with Claude Code