Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,14 +391,23 @@ func (b *Bucket) MoveBucket(key []byte, dstBucket *Bucket) (err error) {
return errors.ErrIncompatibleValue
}

// remove the sub-bucket from the source bucket
// remove the sub-bucket from the source bucket. If it was materialized
// earlier in this transaction it may hold pending changes in memory, so keep
// a reference to carry over to the destination below.
movedChild := b.buckets[string(newKey)]
delete(b.buckets, string(newKey))
c.node().del(newKey)

// add te sub-bucket to the destination bucket
newValue := cloneBytes(v)
curDst.node().put(newKey, newKey, newValue, 0, common.BucketLeafFlag)

// carry the materialized sub-bucket over so its pending changes are spilled
// under the destination at commit; otherwise they would be silently lost.
if movedChild != nil && dstBucket.buckets != nil {
dstBucket.buckets[string(newKey)] = movedChild
}

return nil
}

Expand Down
65 changes: 65 additions & 0 deletions movebucket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,68 @@ func populateSampleDataInBucket(t testing.TB, bk *bbolt.Bucket, n int) {
require.NoError(t, err)
}
}

// TestTx_MoveBucket_PendingChanges verifies that changes made to a sub-bucket in
// the same transaction as a MoveBucket are preserved rather than silently dropped.
func TestTx_MoveBucket_PendingChanges(t *testing.T) {
t.Run("modify committed bucket, then move in same tx", func(t *testing.T) {
db := btesting.MustCreateDB(t)

// tx1: create src/dst and a child with a committed key.
require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
src, err := tx.CreateBucket([]byte("src"))
require.NoError(t, err)
if _, err := tx.CreateBucket([]byte("dst")); err != nil {
return err
}
child, err := src.CreateBucket([]byte("child"))
require.NoError(t, err)
return child.Put([]byte("committed"), []byte("v1"))
}))

// tx2: add a key to the child, then move it, in the same transaction.
require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
src := tx.Bucket([]byte("src"))
require.NoError(t, src.Bucket([]byte("child")).Put([]byte("pending"), []byte("v2")))
return tx.MoveBucket([]byte("child"), src, tx.Bucket([]byte("dst")))
}))

require.NoError(t, db.View(func(tx *bbolt.Tx) error {
child := tx.Bucket([]byte("dst")).Bucket([]byte("child"))
require.NotNil(t, child)
require.Equal(t, []byte("v1"), child.Get([]byte("committed")))
require.Equal(t, []byte("v2"), child.Get([]byte("pending")))
return nil
}))
})

t.Run("create, populate and move in the same tx", func(t *testing.T) {
db := btesting.MustCreateDB(t)

require.NoError(t, db.Update(func(tx *bbolt.Tx) error {
src, err := tx.CreateBucket([]byte("src"))
require.NoError(t, err)
dst, err := tx.CreateBucket([]byte("dst"))
require.NoError(t, err)

child, err := src.CreateBucket([]byte("child"))
require.NoError(t, err)
require.NoError(t, child.Put([]byte("k"), []byte("v")))
// a nested sub-bucket with its own data
grand, err := child.CreateBucket([]byte("grand"))
require.NoError(t, err)
require.NoError(t, grand.Put([]byte("gk"), []byte("gv")))

return src.MoveBucket([]byte("child"), dst)
}))

require.NoError(t, db.View(func(tx *bbolt.Tx) error {
require.Nil(t, tx.Bucket([]byte("src")).Bucket([]byte("child")))
child := tx.Bucket([]byte("dst")).Bucket([]byte("child"))
require.NotNil(t, child)
require.Equal(t, []byte("v"), child.Get([]byte("k")))
require.Equal(t, []byte("gv"), child.Bucket([]byte("grand")).Get([]byte("gk")))
return nil
}))
})
}