Skip to content

fix(libravdb): make EnsureCollection non-destructive - #4

Merged
xDarkicex merged 4 commits into
xDarkicex:masterfrom
jason-allen-oneal:fix/ensurecollection-nondestructive
Jun 30, 2026
Merged

fix(libravdb): make EnsureCollection non-destructive#4
xDarkicex merged 4 commits into
xDarkicex:masterfrom
jason-allen-oneal:fix/ensurecollection-nondestructive

Conversation

@jason-allen-oneal

@jason-allen-oneal jason-allen-oneal commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Description

Refactors EnsureCollection so an existing collection with an incompatible vector dimension is no longer dropped and recreated by default. Instead, callers get a typed CollectionDimensionMismatchError wrapping ErrDimensionMismatch, preserving the existing collection and its data.

Also fixes the local go vet ./... blocker by storing typed *EdgeTablePage values in the graph page registry instead of round-tripping Go pointers through uintptr.

Related Issue

Fixes #5.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Performance improvement
  • Documentation update
  • Code refactoring
  • Test improvements

Changes Made

  • Added/modified functionality
  • Updated documentation
  • Added/updated tests
  • Performance optimizations
  • Bug fixes

Testing

  • Unit tests pass locally
  • Integration tests pass locally
  • Added tests for new functionality
  • Benchmarks run (if performance-related)
  • Manual testing completed

Commands run:

  • go build -v $(go list ./... | grep -v '/examples\|/benchmark\|/tests')
  • go vet ./...
  • golangci-lint run --timeout=5m
  • go test -v -short -timeout=5m -coverprofile=coverage.out $(go list ./... | grep -v '/examples\|/benchmark\|/tests')
  • go test -v -timeout=10m -tags=integration ./tests/...
  • ./scripts/validate-examples.sh
  • go test ./libravdb -run 'TestEnsureCollection'
  • go test ./internal/storage/singlefile
  • go test ./internal/graph ./libravdb -run 'TestEnsureCollection|TestBFS|TestGraph|TestEdgeTable|TestWAL|TestKindSet|TestMetrics|TestReverseIndex|TestDropNodeEdges|TestSegment|TestManifest'

Note: ./scripts/lint.sh now passes formatting, goimports, and go vet; its final repository-wide heuristic check still exits non-zero on pre-existing broad warnings: one TODO, ignored-error patterns, and many fmt.Sprintf calls. The CI lint command, golangci-lint run --timeout=5m, passes locally.

Performance Impact

No expected performance impact. This changes the mismatch control path for collection setup and removes unsafe pointer storage from the graph registry.

Breaking Changes

This intentionally prevents the previous silent destructive behavior in EnsureCollection. Code that relied on automatic deletion/recreation should switch to EnsureCollectionRecreateOnDimensionMismatch.

Checklist

  • My code follows the project style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Additional Notes

  • Existing matching collections keep current behavior.
  • Incompatible existing collections survive unchanged and return a typed mismatch error.
  • Explicit recreation remains available through EnsureCollectionRecreateOnDimensionMismatch.
  • Related issue/PR search found no existing open or closed issue for this specific data-loss path.

Summary by CodeRabbit

  • New Features

    • Added a clearer collection-dimension mismatch error, including the collection name and both dimensions.
    • Added an option to recreate a collection when its stored dimension does not match the requested one.
  • Bug Fixes

    • Existing collections marked as deleted can now be created again properly.
    • Ensured mismatched-dimension checks no longer overwrite data unless recreation is explicitly requested.
  • Tests

    • Added coverage for both non-destructive mismatch handling and explicit recreation behavior.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9bbfd83f-4264-41fe-b9e9-53a01410400e

📥 Commits

Reviewing files that changed from the base of the PR and between f4d2631 and f1c0fcf.

📒 Files selected for processing (1)
  • libravdb/collection_management_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • libravdb/collection_management_test.go

📝 Walkthrough

Walkthrough

EnsureCollection is split into a non-destructive default (returns CollectionDimensionMismatchError on dimension mismatch) and an explicit EnsureCollectionRecreateOnDimensionMismatch variant. A new CollectionDimensionMismatchError type is added. Storage replay is fixed to allow overwriting deleted collections. Two new tests cover both behaviors.

Collection Dimension Mismatch API

Layer / File(s) Summary
CollectionDimensionMismatchError type
libravdb/errors.go
New exported struct with Error() and Unwrap() (returning ErrDimensionMismatch) representing an existing collection's dimension conflicting with a requested dimension.
EnsureCollection API refactor
libravdb/database.go
EnsureCollection becomes non-destructive, returning CollectionDimensionMismatchError on mismatch. New EnsureCollectionRecreateOnDimensionMismatch performs the former destructive behavior. Both delegate to a shared internal ensureCollection; race resolution updated to return the typed error; helper functions newCollectionDimensionMismatchError and ensureCollectionOptions added.
Storage fix for deleted collection recreation
internal/storage/singlefile/engine.go
applyCreateCollection early-return condition changed to skip only non-deleted existing collections, allowing a previously deleted collection to be overwritten during replay/commit.
New collection management tests
libravdb/collection_management_test.go, tests/main_test.go
TestEnsureCollectionDimensionMismatchIsNonDestructive asserts the original collection and inserted vector survive a mismatched EnsureCollection call. TestEnsureCollectionRecreateOnDimensionMismatchOptIn asserts the opt-in variant recreates the collection empty with the new dimension after a close/reopen cycle.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 A mismatch once silently wiped data away,
Now a typed error keeps old vectors at play.
Opt-in to destroy, or keep what you've got,
The deleted-flag fix ties the storage's knot.
Safe defaults for all — what a wonderful day!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The graph registry go vet fix is unrelated to issue #5 and adds extra scope beyond the linked collection-mismatch fix. Move the go vet/uintptr registry change to a separate PR or add a linked issue for that work so this PR stays focused on #5.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change to make EnsureCollection non-destructive.
Description check ✅ Passed The PR description follows the template and covers the change, related issue, testing, breaking changes, checklist, and notes.
Linked Issues check ✅ Passed The changes satisfy #5 by preserving existing collections on dimension mismatch, returning a typed mismatch error, and adding regression tests.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jason-allen-oneal

Copy link
Copy Markdown
Contributor Author

Issue/PR cross-check before review:

Why this is worth fixing:

  • The old default silently deleted and recreated an existing collection when the requested dimension changed.
  • A wrong embedding-model/config dimension is a plausible operator mistake, and the current behavior converts that mistake into silent data loss.
  • The new default fails closed with a typed CollectionDimensionMismatchError while preserving errors.Is(err, ErrDimensionMismatch) compatibility.
  • The destructive recovery behavior remains available, but only through the explicit EnsureCollectionRecreateOnDimensionMismatch API.
  • Regression coverage proves the original collection and vector survive a mismatch unchanged.

@jason-allen-oneal
jason-allen-oneal marked this pull request as ready for review June 29, 2026 22:48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
libravdb/collection_management_test.go (1)

71-100: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Exercise the restart/replay path in this regression test.

This only proves the in-process recreate flow. The storage fix is in internal/storage/singlefile/engine.go, so closing and reopening before the final assertions would guard the persisted recovery contract too.

Suggested expansion
 func TestEnsureCollectionRecreateOnDimensionMismatchOptIn(t *testing.T) {
 	ctx := context.Background()
-	db, err := Open(WithStoragePath(testDBPath(t)))
+	path := testDBPath(t)
+	db, err := Open(WithStoragePath(path))
 	if err != nil {
 		t.Fatalf("Failed to create database: %v", err)
 	}
-	defer db.Close()
+	t.Cleanup(func() { _ = db.Close() })
@@
 	recreated, err := db.EnsureCollectionRecreateOnDimensionMismatch(ctx, "embeddings", 4, WithFlat())
 	if err != nil {
 		t.Fatalf("EnsureCollectionRecreateOnDimensionMismatch: %v", err)
 	}
 	if recreated == collection {
 		t.Fatal("Expected collection to be recreated through explicit opt-in")
 	}
-	if recreated.Dimension() != 4 {
-		t.Fatalf("Expected recreated dimension 4, got %d", recreated.Dimension())
+	if err := db.Close(); err != nil {
+		t.Fatalf("Close before reopen: %v", err)
+	}
+	db, err = Open(WithStoragePath(path))
+	if err != nil {
+		t.Fatalf("Reopen after recreate: %v", err)
+	}
+	recreated, err = db.GetCollection("embeddings")
+	if err != nil {
+		t.Fatalf("GetCollection after reopen: %v", err)
+	}
+	if recreated.Dimension() != 4 {
+		t.Fatalf("Expected recreated dimension 4 after reopen, got %d", recreated.Dimension())
 	}
 	if stats := recreated.Stats(ctx); stats.VectorCount != 0 {
 		t.Fatalf("Expected recreated collection to start empty, got %d vectors", stats.VectorCount)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libravdb/collection_management_test.go` around lines 71 - 100, Update
TestEnsureCollectionRecreateOnDimensionMismatchOptIn to exercise the persisted
restart/replay path, not just the in-process recreate flow. After calling
EnsureCollectionRecreateOnDimensionMismatch and before the final
dimension/vector-count assertions, close the database and reopen it with Open
using the same storage path, then fetch the collection again and assert the
recreated state from the reopened handle. This will validate the storage
recovery behavior in internal/storage/singlefile/engine.go through the
EnsureCollectionRecreateOnDimensionMismatch and Stats flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@libravdb/collection_management_test.go`:
- Around line 71-100: Update
TestEnsureCollectionRecreateOnDimensionMismatchOptIn to exercise the persisted
restart/replay path, not just the in-process recreate flow. After calling
EnsureCollectionRecreateOnDimensionMismatch and before the final
dimension/vector-count assertions, close the database and reopen it with Open
using the same storage path, then fetch the collection again and assert the
recreated state from the reopened handle. This will validate the storage
recovery behavior in internal/storage/singlefile/engine.go through the
EnsureCollectionRecreateOnDimensionMismatch and Stats flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 667585ae-289f-445c-a5c7-6026cdd9591e

📥 Commits

Reviewing files that changed from the base of the PR and between 037e8d4 and f4d2631.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (6)
  • internal/graph/registry.go
  • internal/storage/singlefile/engine.go
  • libravdb/collection_management_test.go
  • libravdb/database.go
  • libravdb/errors.go
  • tests/main_test.go

@xDarkicex xDarkicex left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey, really appreciate the EnsureCollection fix — making dimension mismatch non-destructive is exactly the right call and the tests look thorough.

one thing that needs a revert though: the registry.go change from uintptr to *EdgeTablePage. that pointer comes from off-heap mmap allocated memory (ShardedFreeList), see store.go line 232:

page = (*EdgeTablePage)(unsafe.Pointer(&slotBytes[64]))

storing it as *EdgeTablePage in a Go map tells the GC "this is a real Go pointer, trace it". but the memory backing it is mmap off-heap, managed by the memory package. if the GC concurrent sweep touches that region after Retire has reclaimed it and the backing pages get unmapped, you get a SIGSEGV with zero useful stack trace. uintptr is invisible to the GC — that was the whole reason it was used, not as a workaround for the vet check.

the Go spec is pretty explicit about this — uintptr conversions are valid but the result is not a tracked pointer. the original code was correct. i would revert the registry.go file entirely back to the original uintptr approach and keep everything else as-is.

rest of the PR looks solid, thanks for catching the dimension mismatch footgun.

@xDarkicex xDarkicex added the bump:patch Patch version bump on merge label Jun 30, 2026
@xDarkicex
xDarkicex merged commit 6609040 into xDarkicex:master Jun 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bump:patch Patch version bump on merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: EnsureCollection silently recreates collections on dimension mismatch

2 participants