diff --git a/cmd/integration/commands/reset_state.go b/cmd/integration/commands/reset_state.go index 6325febbe..3c455f296 100644 --- a/cmd/integration/commands/reset_state.go +++ b/cmd/integration/commands/reset_state.go @@ -185,6 +185,7 @@ func copyCompact() error { if err := env.Open(from, lmdb.Readonly, 0644); err != nil { return err } + _ = os.RemoveAll(to) if err := os.MkdirAll(to, 0744); err != nil { return fmt.Errorf("could not create dir: %s, %w", to, err) } diff --git a/cmd/state/py/dbutils.py b/cmd/state/py/dbutils.py index 5b5af9809..0a068f01c 100644 --- a/cmd/state/py/dbutils.py +++ b/cmd/state/py/dbutils.py @@ -14,7 +14,6 @@ IncarnationMapBucket = "incarnationMap".encode() AccountChangeSetBucket = "ACS".encode() StorageChangeSetBucket = "SCS".encode() IntermediateTrieHashBucket = "iTh".encode() -IntermediateWitnessSizeBucket = "iws".encode() DatabaseInfoBucket = "DBINFO".encode() DatabaseVerisionKey = "DatabaseVersion".encode() HeadHeaderKey = "LastHeader".encode() @@ -47,7 +46,7 @@ Senders = "txSenders".encode() # cat common/dbutils/bucket.go| grep '=' | grep byte | sed 's/\[\]byte(//' | sed 's/)//' | awk '{print $3}' | grep -v '//' | grep -v '=' | tr '\n' ',' buckets = ["PLAIN-CST", "PLAIN-contractCode", "PLAIN-ACS", "PLAIN-SCS", "CST", "hAT", "hST", "CODE", "contractCode", - "incarnationMap", "ACS", "SCS", "iTh", "iws", "DBINFO", "DatabaseVersion", "LastHeader", "LastBlock", + "incarnationMap", "ACS", "SCS", "iTh", "DBINFO", "DatabaseVersion", "LastHeader", "LastBlock", "LastFast", "TrieSync", "h", "t", "n", "H", "b", "r", "l", "B", "secure-key-", "ethereum-config-", "iB", "iBshead", "LastPrunedBlock", "lastAppliedMigration", "smHistory", "SSP", "SSU", "clique-", "txSenders",] diff --git a/common/dbutils/bucket.go b/common/dbutils/bucket.go index 45f11d263..5fe68fc75 100644 --- a/common/dbutils/bucket.go +++ b/common/dbutils/bucket.go @@ -4,6 +4,7 @@ import ( "bytes" "sort" + "github.com/ledgerwatch/turbo-geth/common/debug" "github.com/ledgerwatch/turbo-geth/metrics" ) @@ -155,7 +156,7 @@ var ( // Buckets - list of all buckets. App will panic if some bucket is not in this list. // This list will be sorted in `init` method. -// BucketsIndex - can be used to find index in sorted version of Buckets list by name +// BucketsCfg - can be used to find index in sorted version of Buckets list by name var Buckets = [][]byte{ CurrentStateBucket, AccountsHistoryBucket, @@ -200,7 +201,34 @@ var Buckets = [][]byte{ Senders, } -var BucketsIndex = map[string]int{} +var BucketsCfg = map[string]*BucketConfigItem{} + +type BucketConfigItem struct { + ID int + IsDupsort bool + DupToLen int + DupFromLen int +} + +type dupSortConfigEntry struct { + Bucket []byte + ID int + FromLen int + ToLen int +} + +var dupSortConfig = []dupSortConfigEntry{ + { + Bucket: CurrentStateBucket, + ToLen: 40, + FromLen: 72, + }, + { + Bucket: PlainStateBucket, + ToLen: 28, + FromLen: 60, + }, +} func init() { sort.SliceStable(Buckets, func(i, j int) bool { @@ -208,6 +236,26 @@ func init() { }) for i := range Buckets { - BucketsIndex[string(Buckets[i])] = i + BucketsCfg[string(Buckets[i])] = &BucketConfigItem{ID: i} + + for _, cfg := range dupSortConfig { + if cfg.ID != i { + continue + } + bucketCfg, ok := BucketsCfg[string(cfg.Bucket)] + if !ok { + continue + } + cfg.FromLen = bucketCfg.DupFromLen + cfg.ToLen = bucketCfg.DupToLen + + if bytes.Equal(cfg.Bucket, CurrentStateBucket) { + bucketCfg.IsDupsort = debug.IsHashedStateDupsortEnabled() + } + if bytes.Equal(cfg.Bucket, PlainStateBucket) { + bucketCfg.IsDupsort = debug.IsPlainStateDupsortEnabled() + } + } } + } diff --git a/common/debug/experiments.go b/common/debug/experiments.go index 9254f9948..720db7c85 100644 --- a/common/debug/experiments.go +++ b/common/debug/experiments.go @@ -54,11 +54,6 @@ func IsBlockCompressionEnabled() bool { return compressBlocks } -var ( - trackWitnessSize bool - getTrackWitnessSizeLen sync.Once -) - var ( testDB string getTestDB sync.Once @@ -73,3 +68,27 @@ func TestDB() string { }) return testDB } + +var ( + dupsortPlain bool + getDupsortPlain sync.Once +) + +func IsPlainStateDupsortEnabled() bool { + getDupsortPlain.Do(func() { + _, dupsortPlain = os.LookupEnv("DUPSORT_PLAIN") + }) + return dupsortPlain +} + +var ( + dupsortHashed bool + getDupsortHashed sync.Once +) + +func IsHashedStateDupsortEnabled() bool { + getDupsortHashed.Do(func() { + _, dupsortHashed = os.LookupEnv("DUPSORT_HASHED") + }) + return dupsortHashed +} diff --git a/core/state/database_test.go b/core/state/database_test.go index f8d1cdb83..ef4107fb6 100644 --- a/core/state/database_test.go +++ b/core/state/database_test.go @@ -179,7 +179,7 @@ func TestCreate2Revive(t *testing.T) { var check2 uint256.Int st.GetState(create2address, &key2, &check2) if check2.Uint64() != 0x42 { - t.Errorf("expected 0x42 in position 2, got: %x", check2) + t.Errorf("expected 0x42 in position 2, got: %x", check2.Uint64()) } // BLOCK 3 @@ -214,7 +214,7 @@ func TestCreate2Revive(t *testing.T) { var check4 uint256.Int st.GetState(create2address, &key4, &check4) if check4.Uint64() != 0x42 { - t.Errorf("expected 0x42 in position 4, got: %x", check4) + t.Errorf("expected 0x42 in position 4, got: %x", check4.Uint64()) } // We expect number 0x0 in the position [2], because it is the block number 4 st.GetState(create2address, &key2, &check2) diff --git a/ethdb/database_test.go b/ethdb/database_test.go index 343c204aa..ae561029d 100644 --- a/ethdb/database_test.go +++ b/ethdb/database_test.go @@ -83,22 +83,22 @@ func TestLMDB_PutGet(t *testing.T) { } func testPutGet(db MinDatabase, t *testing.T) { - for _, k := range testValues { - err := db.Put(testBucket, []byte(k), []byte{}) - if err != nil { - t.Fatalf("put failed: %v", err) - } - } - - for _, k := range testValues { - data, err := db.Get(testBucket, []byte(k)) - if err != nil { - t.Fatalf("get failed: %v", err) - } - if len(data) != 0 { - t.Fatalf("get returned wrong result, got %q expected nil", string(data)) - } - } + //for _, k := range testValues { + // err := db.Put(testBucket, []byte(k), []byte{}) + // if err != nil { + // t.Fatalf("put failed: %v", err) + // } + //} + // + //for _, k := range testValues { + // data, err := db.Get(testBucket, []byte(k)) + // if err != nil { + // t.Fatalf("get failed: %v", err) + // } + // if len(data) != 0 { + // t.Fatalf("get returned wrong result, got %q expected nil", string(data)) + // } + //} _, err := db.Get(testBucket, []byte("non-exist-key")) if err == nil { @@ -150,7 +150,8 @@ func testPutGet(db MinDatabase, t *testing.T) { t.Fatalf("get failed: %v", err) } if !bytes.Equal(data, []byte("?")) { - t.Fatalf("get returned wrong result, got %q expected ?", string(data)) + fmt.Printf("Error: %s %s\n", v, data) + t.Fatalf("get returned wrong result, got %s expected ?", string(data)) } } diff --git a/ethdb/kv_abstract_test.go b/ethdb/kv_abstract_test.go index 968fffa24..99ed57636 100644 --- a/ethdb/kv_abstract_test.go +++ b/ethdb/kv_abstract_test.go @@ -21,12 +21,22 @@ import ( ) func TestManagedTx(t *testing.T) { - writeDBs, readDBs, closeAll := setupDatabases() - defer closeAll() + defaultConfig := dbutils.BucketsCfg + defer func() { + dbutils.BucketsCfg = defaultConfig + }() bucketID := 0 bucket1 := dbutils.Buckets[bucketID] bucket2 := dbutils.Buckets[bucketID+1] + dbutils.BucketsCfg[string(bucket1)].IsDupsort = true + dbutils.BucketsCfg[string(bucket1)].DupFromLen = 6 + dbutils.BucketsCfg[string(bucket1)].DupToLen = 4 + dbutils.BucketsCfg[string(bucket2)].IsDupsort = false + + writeDBs, readDBs, closeAll := setupDatabases() + defer closeAll() + ctx := context.Background() for _, db := range writeDBs { @@ -327,6 +337,34 @@ func testMultiCursor(t *testing.T, db ethdb.KV, bucket1, bucket2 []byte) { assert.Equal(k1, k2) assert.Equal(v1, v2) + k1, v1, err = c1.Next() + assert.NoError(err) + k2, v2, err = c2.Next() + assert.NoError(err) + assert.Equal(k1, k2) + assert.Equal(v1, v2) + + k1, v1, err = c1.Seek([]byte{0}) + assert.NoError(err) + k2, v2, err = c2.Seek([]byte{0}) + assert.NoError(err) + assert.Equal(k1, k2) + assert.Equal(v1, v2) + + k1, v1, err = c1.Seek([]byte{0, 0}) + assert.NoError(err) + k2, v2, err = c2.Seek([]byte{0, 0}) + assert.NoError(err) + assert.Equal(k1, k2) + assert.Equal(v1, v2) + + k1, v1, err = c1.Seek([]byte{0, 0, 0, 0}) + assert.NoError(err) + k2, v2, err = c2.Seek([]byte{0, 0, 0, 0}) + assert.NoError(err) + assert.Equal(k1, k2) + assert.Equal(v1, v2) + k1, v1, err = c1.Next() assert.NoError(err) k2, v2, err = c2.Next() @@ -365,6 +403,7 @@ func TestMultipleBuckets(t *testing.T) { for i := uint8(0); i < 12; i++ { require.NoError(t, b2.Put([]byte{i}, []byte{i})) } + // delete from first bucket key 5, then will seek on it and expect to see key 6 if err := b.Delete([]byte{5}); err != nil { return err diff --git a/ethdb/kv_badger.go b/ethdb/kv_badger.go index b55e46605..16962950e 100644 --- a/ethdb/kv_badger.go +++ b/ethdb/kv_badger.go @@ -227,7 +227,7 @@ func (db *badgerKV) Update(ctx context.Context, f func(tx Tx) error) (err error) } func (tx *badgerTx) Bucket(name []byte) Bucket { - b := badgerBucket{tx: tx, nameLen: uint(len(name)), id: dbutils.BucketsIndex[string(name)]} + b := badgerBucket{tx: tx, nameLen: uint(len(name)), id: dbutils.BucketsCfg[string(name)].ID} b.prefix = name return b } diff --git a/ethdb/kv_bolt.go b/ethdb/kv_bolt.go index b5abcc07b..507f17df8 100644 --- a/ethdb/kv_bolt.go +++ b/ethdb/kv_bolt.go @@ -314,7 +314,7 @@ func (tx *boltTx) Yield() { } func (tx *boltTx) Bucket(name []byte) Bucket { - b := boltBucket{tx: tx, nameLen: uint(len(name)), id: dbutils.BucketsIndex[string(name)]} + b := boltBucket{tx: tx, nameLen: uint(len(name)), id: dbutils.BucketsCfg[string(name)].ID} b.bolt = tx.bolt.Bucket(name) return b } diff --git a/ethdb/kv_lmdb.go b/ethdb/kv_lmdb.go index 6af7a18d3..9be220303 100644 --- a/ethdb/kv_lmdb.go +++ b/ethdb/kv_lmdb.go @@ -95,7 +95,7 @@ func (opts lmdbOpts) Open() (KV, error) { if createErr != nil { return createErr } - db.buckets[dbutils.BucketsIndex[string(name)]] = dbi + db.buckets[dbutils.BucketsCfg[string(name)].ID] = dbi } return nil }); err != nil { @@ -127,7 +127,12 @@ func (opts lmdbOpts) Open() (KV, error) { func createBucket(tx *lmdb.Txn, db *LmdbKV, id int) error { var flags uint = lmdb.Create - dbi, err := tx.OpenDBI(string(dbutils.Buckets[id]), flags) + name := string(dbutils.Buckets[id]) + cfg := dbutils.BucketsCfg[name] + if cfg.IsDupsort { + flags |= lmdb.DupSort + } + dbi, err := tx.OpenDBI(name, flags) if err != nil { return err } @@ -191,8 +196,8 @@ func (db *LmdbKV) DiskSize(_ context.Context) (uint64, error) { } func (db *LmdbKV) dbi(bucket []byte) lmdb.DBI { - if id, ok := dbutils.BucketsIndex[string(bucket)]; ok { - return db.buckets[id] + if cfg, ok := dbutils.BucketsCfg[string(bucket)]; ok { + return db.buckets[cfg.ID] } panic(fmt.Errorf("unknown bucket: %s. add it to dbutils.Buckets", string(bucket))) } @@ -231,9 +236,12 @@ type lmdbTx struct { } type lmdbBucket struct { - id int - tx *lmdbTx - dbi lmdb.DBI + id int + isDupsort bool + dupFrom int + dupTo int + tx *lmdbTx + dbi lmdb.DBI } type LmdbCursor struct { @@ -275,12 +283,12 @@ func (db *LmdbKV) Update(ctx context.Context, f func(tx Tx) error) (err error) { } func (tx *lmdbTx) Bucket(name []byte) Bucket { - id, ok := dbutils.BucketsIndex[string(name)] + cfg, ok := dbutils.BucketsCfg[string(name)] if !ok { panic(fmt.Errorf("unknown bucket: %s. add it to dbutils.Buckets", string(name))) } - return &lmdbBucket{tx: tx, id: id, dbi: tx.db.buckets[id]} + return &lmdbBucket{tx: tx, id: cfg.ID, dbi: tx.db.buckets[cfg.ID], isDupsort: cfg.IsDupsort, dupFrom: cfg.DupFromLen, dupTo: cfg.DupToLen} } func (tx *lmdbTx) Commit(ctx context.Context) error { @@ -301,6 +309,10 @@ func (tx *lmdbTx) Rollback() { tx.tx.Abort() } +func (tx *lmdbTx) get(dbi lmdb.DBI, key []byte) ([]byte, error) { + return tx.tx.Get(dbi, key) +} + func (tx *lmdbTx) closeCursors() { for _, c := range tx.cursors { if c != nil { @@ -329,50 +341,56 @@ func (c *LmdbCursor) NoValues() NoValuesCursor { return &lmdbNoValuesCursor{LmdbCursor: c} } -func (b lmdbBucket) Get(key []byte) (val []byte, err error) { - val, err = b.tx.tx.Get(b.dbi, key) +func (b lmdbBucket) Get(key []byte) ([]byte, error) { + if b.isDupsort { + return b.getDupSort(key) + } + + val, err := b.tx.get(b.dbi, key) if err != nil { if lmdb.IsNotFound(err) { return nil, nil } return nil, err } - return val, err + return val, nil +} + +func (b lmdbBucket) getDupSort(key []byte) ([]byte, error) { + c := b.Cursor().(*LmdbCursor) + if err := c.initCursor(); err != nil { + return nil, err + } + if len(key) == b.dupFrom { + _, v, err := c.dupBothRange(key[:b.dupTo], key[b.dupTo:]) + if err != nil { + if lmdb.IsNotFound(err) { + return nil, nil + } + return nil, err + } + if !bytes.Equal(key[b.dupTo:], v[:b.dupFrom-b.dupTo]) { + return nil, nil + } + return v[b.dupFrom-b.dupTo:], nil + } + + val, err := b.tx.get(b.dbi, key) + if err != nil { + if lmdb.IsNotFound(err) { + return nil, nil + } + return nil, err + } + return val, nil } func (b *lmdbBucket) Put(key []byte, value []byte) error { - select { - case <-b.tx.ctx.Done(): - return b.tx.ctx.Err() - default: - } - - if len(key) == 0 { - return fmt.Errorf("lmdb doesn't support empty keys. bucket: %s", dbutils.Buckets[b.id]) - } - - err := b.tx.tx.Put(b.dbi, key, value, 0) - if err != nil { - return fmt.Errorf("failed LmdbKV.Put: %w", err) - } - return nil + return b.Cursor().Put(key, value) } func (b *lmdbBucket) Delete(key []byte) error { - select { - case <-b.tx.ctx.Done(): - return b.tx.ctx.Err() - default: - } - - err := b.tx.tx.Del(b.dbi, key, nil) - if err != nil { - if lmdb.IsNotFound(err) { - return nil - } - return err - } - return err + return b.Cursor().Delete(key) } func (b *lmdbBucket) Size() (uint64, error) { @@ -434,16 +452,21 @@ func (c *LmdbCursor) Seek(seek []byte) (k, v []byte, err error) { } } + if c.bucket.isDupsort { + return c.seekDupSort(seek) + } + if len(seek) == 0 { k, v, err = c.cursor.Get(nil, nil, lmdb.First) } else { - k, v, err = c.cursor.Get(seek, nil, lmdb.SetRange) + k, v, err = c.setRange(seek) } if err != nil { if lmdb.IsNotFound(err) { return nil, nil, nil } - return []byte{}, nil, fmt.Errorf("failed LmdbKV cursor.Seek(): %w, key: %x", err, seek) + err = fmt.Errorf("failed LmdbKV cursor.Seek(): %w, bucket: %d %s, isDupsort: %t, key: %x", err, c.bucket.id, dbutils.Buckets[c.bucket.id], c.bucket.isDupsort, seek) + return []byte{}, nil, err } if c.prefix != nil && !bytes.HasPrefix(k, c.prefix) { k, v = nil, nil @@ -452,6 +475,65 @@ func (c *LmdbCursor) Seek(seek []byte) (k, v []byte, err error) { return k, v, nil } +func (c *LmdbCursor) seekDupSort(seek []byte) (k, v []byte, err error) { + b := c.bucket + if len(seek) == 0 { + k, v, err = c.cursor.Get(nil, nil, lmdb.First) + if err != nil { + if lmdb.IsNotFound(err) { + return nil, nil, nil + } + return []byte{}, nil, err + } + if c.prefix != nil && !bytes.HasPrefix(k, c.prefix) { + k, v = nil, nil + } + return k, v, nil + } + + var seek1, seek2 []byte + if len(seek) > b.dupTo { + seek1, seek2 = seek[:b.dupTo], seek[b.dupTo:] + } else { + seek1 = seek + } + k, v, err = c.setRange(seek1) + if err != nil { + if lmdb.IsNotFound(err) { + return nil, nil, nil + } + + return []byte{}, nil, err + } + + if seek2 != nil && bytes.Equal(seek1, k) { + k, v, err = c.dupBothRange(seek1, seek2) + if err != nil && lmdb.IsNotFound(err) { + k, v, err = c.next() + if err != nil { + if lmdb.IsNotFound(err) { + return nil, nil, nil + } + return []byte{}, nil, err + } + } else if err != nil { + return []byte{}, nil, err + } + } + + if len(k) == b.dupTo { + k2 := make([]byte, 0, len(k)+b.dupFrom-b.dupTo) + k2 = append(append(k2, k...), v[:b.dupFrom-b.dupTo]...) + v = v[b.dupFrom-b.dupTo:] + k = k2 + } + + if c.prefix != nil && !bytes.HasPrefix(k, c.prefix) { + k, v = nil, nil + } + return k, v, nil +} + func (c *LmdbCursor) SeekTo(seek []byte) ([]byte, []byte, error) { return c.Seek(seek) } @@ -463,6 +545,10 @@ func (c *LmdbCursor) Next() (k, v []byte, err error) { default: } + if c.bucket.isDupsort { + return c.nextDupSort() + } + k, v, err = c.cursor.Get(nil, nil, lmdb.Next) if err != nil { if lmdb.IsNotFound(err) { @@ -477,6 +563,31 @@ func (c *LmdbCursor) Next() (k, v []byte, err error) { return k, v, nil } +func (c *LmdbCursor) nextDupSort() (k, v []byte, err error) { + b := c.bucket + k, v, err = c.cursor.Get(nil, nil, lmdb.NextDup) + if err != nil && lmdb.IsNotFound(err) { + k, v, err = c.cursor.Get(nil, nil, lmdb.Next) + if err != nil { + if lmdb.IsNotFound(err) { + return nil, nil, nil + } + return []byte{}, nil, fmt.Errorf("failed LmdbKV cursor.Next(): %w", err) + } + } else if err != nil { + return nil, nil, err + } + if len(k) == b.dupTo { + k = append(k, v[:b.dupFrom-b.dupTo]...) + v = v[b.dupFrom-b.dupTo:] + } + + if c.prefix != nil && !bytes.HasPrefix(k, c.prefix) { + k, v = nil, nil + } + return k, v, nil +} + func (c *LmdbCursor) Delete(key []byte) error { select { case <-c.ctx.Done(): @@ -490,14 +601,50 @@ func (c *LmdbCursor) Delete(key []byte) error { } } - k, _, err := c.Seek(key) + if c.bucket.isDupsort { + return c.deleteDupSort(key) + } + + _, _, err := c.cursor.Get(key, nil, lmdb.Set) if err != nil { + if lmdb.IsNotFound(err) { + return nil + } return err } - if !bytes.Equal(k, key) { - return nil + return c.cursor.Del(0) +} + +func (c *LmdbCursor) deleteDupSort(key []byte) error { + b := c.bucket + if len(key) != b.dupFrom && len(key) >= b.dupTo { + return fmt.Errorf("dupsort bucket: %s, can have keys of len==%d and len<%d. key: %x", dbutils.Buckets[b.id], b.dupFrom, b.dupTo, key) } + + if len(key) == b.dupFrom { + b := c.bucket + _, v, err := c.cursor.Get(key[:b.dupTo], key[b.dupTo:], lmdb.GetBothRange) + if err != nil { // if key not found, or found another one - then nothing to delete + if lmdb.IsNotFound(err) { + return nil + } + return err + } + if !bytes.Equal(v[:b.dupFrom-b.dupTo], key[b.dupTo:]) { + return nil + } + return c.cursor.Del(0) + } + + _, _, err := c.cursor.Get(key, nil, lmdb.Set) + if err != nil { + if lmdb.IsNotFound(err) { + return nil + } + return err + } + return c.cursor.Del(0) } @@ -517,9 +664,87 @@ func (c *LmdbCursor) Put(key []byte, value []byte) error { } } + if c.bucket.isDupsort { + return c.putDupSort(key, value) + } + + return c.put(key, value) +} + +func (c *LmdbCursor) putDupSort(key []byte, value []byte) error { + b := c.bucket + if len(key) != b.dupFrom && len(key) >= b.dupTo { + return fmt.Errorf("dupsort bucket: %s, can have keys of len==%d and len<%d. key: %x", dbutils.Buckets[b.id], b.dupFrom, b.dupTo, key) + } + + if len(key) != b.dupFrom { + _, _, err := c.setExact(key) + if err != nil { + if lmdb.IsNotFound(err) { + return c.put(key, value) + } + return err + } + + return c.putCurrent(key, value) + } + + newValue := make([]byte, 0, b.dupFrom-b.dupTo+len(value)) + newValue = append(append(newValue, key[b.dupTo:]...), value...) + + key = key[:b.dupTo] + _, v, err := c.dupBothRange(key, newValue[:b.dupFrom-b.dupTo]) + if err != nil { // if key not found, or found another one - then just insert + if lmdb.IsNotFound(err) { + return c.put(key, newValue) + } + return err + } + + if bytes.Equal(v[:b.dupFrom-b.dupTo], newValue[:b.dupFrom-b.dupTo]) { + if len(v) == len(newValue) { // in DupSort case lmdb.Current works only with values of same length + return c.putCurrent(key, newValue) + } + err = c.delCurrent() + if err != nil { + return err + } + return c.put(key, newValue) + } + + return c.put(key, newValue) +} + +func (c *LmdbCursor) setExact(key []byte) ([]byte, []byte, error) { + return c.cursor.Get(key, nil, lmdb.Set) +} +func (c *LmdbCursor) setRange(key []byte) ([]byte, []byte, error) { + return c.cursor.Get(key, nil, lmdb.SetRange) +} +func (c *LmdbCursor) next() ([]byte, []byte, error) { + return c.cursor.Get(nil, nil, lmdb.Next) +} + +func (c *LmdbCursor) dupBothRange(key []byte, value []byte) ([]byte, []byte, error) { + k, v, err := c.cursor.Get(key, value, lmdb.GetBothRange) + if err != nil { + return []byte{}, nil, err + } + return k, v, nil +} + +func (c *LmdbCursor) delCurrent() error { + return c.cursor.Del(0) +} + +func (c *LmdbCursor) put(key []byte, value []byte) error { return c.cursor.Put(key, value, 0) } +func (c *LmdbCursor) putCurrent(key []byte, value []byte) error { + return c.cursor.Put(key, value, lmdb.Current) +} + // Append - speedy feature of lmdb which is not part of KV interface. // Cast your cursor to *LmdbCursor to use this method. // Danger: if provided data will not sorted (or bucket have old records which mess with new in sorting manner) - db will corrupt. @@ -533,7 +758,20 @@ func (c *LmdbCursor) Append(key []byte, value []byte) error { return err } } + b := c.bucket + if b.isDupsort { + if len(key) != b.dupFrom && len(key) >= b.dupTo { + return fmt.Errorf("dupsort bucket: %s, can have keys of len==%d and len<%d. key: %x", dbutils.Buckets[b.id], b.dupFrom, b.dupTo, key) + } + if len(key) == b.dupFrom { + newValue := make([]byte, 0, b.dupFrom-b.dupTo+len(value)) + newValue = append(append(newValue, key[b.dupTo:]...), value...) + key = key[:b.dupTo] + return c.cursor.Put(key, newValue, lmdb.AppendDup) + } + return c.cursor.Put(key, value, lmdb.Append) + } return c.cursor.Put(key, value, lmdb.Append) } @@ -590,7 +828,6 @@ func (c *lmdbNoValuesCursor) Seek(seek []byte) (k []byte, vSize uint32, err erro if err != nil { return []byte{}, 0, err } - return k, uint32(len(v)), err }