DupSort for Plain and Hashed state buckets (behind feature flags) (#854)

* return error from rawdb

* squash

* v14

* improve performance of put

* clean

* clean

* hide feature behind ENV variable

* cleanup

* cleanup

* disable ipc and make Readme less confusing (people thought points are depend on each-other)

* fix test

* cleanup

* cleanup
This commit is contained in:
Alex Sharov 2020-08-02 18:53:08 +07:00 committed by GitHub
parent 6f742c3696
commit ecc94a63f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 424 additions and 80 deletions

View File

@ -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)
}

View File

@ -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",]

View File

@ -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()
}
}
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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))
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}