From 988db642c7ce590da8feac821b778877856556f0 Mon Sep 17 00:00:00 2001 From: Alex Sharov Date: Wed, 1 Nov 2023 15:54:05 +0700 Subject: [PATCH] support empty key/values: 1. as valid dupsort key/values 2. as valid Set/SetRange/GetBoth/GetBothRange arguments. (#123) * save * save * save * save --- mdbx/cursor.go | 18 ++++++- mdbx/cursor_test.go | 127 ++++++++++++++++++++++++++++++++++++++++++++ mdbx/mdbxgo.c | 5 ++ mdbx/mdbxgo.h | 1 + 4 files changed, 150 insertions(+), 1 deletion(-) diff --git a/mdbx/cursor.go b/mdbx/cursor.go index d3f4181..8698396 100644 --- a/mdbx/cursor.go +++ b/mdbx/cursor.go @@ -148,8 +148,10 @@ func (c *Cursor) DBI() DBI { // See mdb_cursor_get. func (c *Cursor) Get(setkey, setval []byte, op uint) (key, val []byte, err error) { switch { - case len(setkey) == 0: + case len(setkey) == 0 && len(setval) == 0: err = c.getVal0(op) + case len(setkey) == 0: + err = c.getVal01(setval, op) case len(setval) == 0: err = c.getVal1(setkey, op) default: @@ -209,6 +211,20 @@ func (c *Cursor) getVal1(setkey []byte, op uint) error { ) return operrno("mdbx_cursor_get", ret) } +func (c *Cursor) getVal01(setval []byte, op uint) error { + var v *C.char + if len(setval) > 0 { + v = (*C.char)(unsafe.Pointer(&setval[0])) + } + ret := C.mdbxgo_cursor_get01( + c._c, + v, C.size_t(len(setval)), + c.txn.key, + c.txn.val, + C.MDBX_cursor_op(op), + ) + return operrno("mdbx_cursor_get", ret) +} // getVal2 retrieves items from the database using key and value data for // reference (GetBoth, GetBothRange, etc). diff --git a/mdbx/cursor_test.go b/mdbx/cursor_test.go index d31d6d9..08676b4 100644 --- a/mdbx/cursor_test.go +++ b/mdbx/cursor_test.go @@ -730,6 +730,133 @@ func TestCursor_Del(t *testing.T) { } } +func TestDupCursor_EmptyKeyValues(t *testing.T) { + env, _ := setup(t) + + var db DBI + err := env.Update(func(txn *Txn) (err error) { + db, err = txn.OpenDBI("testingdup", Create|DupSort, nil, nil) + if err != nil { + return err + } + cur, err := txn.OpenCursor(db) + if err != nil { + return err + } + defer cur.Close() + + // empty value - must function as valid dupsort value + if err = txn.Put(db, []byte{1}, []byte{}, 0); err != nil { + panic(err) + } + if err = txn.Put(db, []byte{1}, []byte{8}, 0); err != nil { + panic(err) + } + + _, v, err := cur.Get([]byte{1}, []byte{}, GetBothRange) + if err != nil { + panic(err) + } + if !bytes.Equal(v, []byte{}) { + panic(v) + } + _, v, err = cur.Get([]byte{1}, []byte{0}, GetBothRange) + if err != nil { + panic(err) + } + if !bytes.Equal(v, []byte{8}) { + panic(v) + } + _, v, err = cur.Get([]byte{}, []byte{0}, GetBoth) + if err == nil { + panic("expecting 'not found' error") + } + if v != nil { + panic(v) + } + + // can use empty key as valid key in non-dupsort operations + k, v, err := cur.Get([]byte{}, nil, SetRange) + if err != nil { + panic(err) + } + if k == nil { + panic("nil") + } + if !bytes.Equal(k, []byte{1}) { + panic(fmt.Sprintf("%x", k)) + } + if !bytes.Equal(v, []byte{}) { + panic(fmt.Sprintf("%x", v)) + } + k, v, err = cur.Get([]byte{}, nil, Set) + if err == nil { + panic("expected 'not found' error") + } + if k != nil { + panic("nil") + } + + // empty key - must function as valid dupsort key + if err = txn.Put(db, []byte{}, []byte{}, 0); err != nil { + panic(err) + } + if err = txn.Put(db, []byte{}, []byte{2}, 0); err != nil { + panic(err) + } + _, v, err = cur.Get([]byte{}, []byte{}, GetBothRange) + if err != nil { + panic(err) + } + if !bytes.Equal(v, []byte{}) { + panic(v) + } + _, v, err = cur.Get([]byte{}, []byte{0}, GetBothRange) + if err != nil { + panic(err) + } + if !bytes.Equal(v, []byte{2}) { + panic(v) + } + _, v, err = cur.Get([]byte{}, []byte{0}, GetBoth) + if err == nil { + panic("expecting 'not found' error ") + } + if v != nil { + panic(v) + } + + // non-existing key + _, v, err = cur.Get([]byte{7}, []byte{}, GetBoth) + if err == nil { + panic("expecting 'not found' error") + } + if v != nil { + panic(v) + } + + // sub-db doesn't have empty value, but we must be able to search from it + if err = txn.Put(db, []byte{2}, []byte{1}, 0); err != nil { + return err + } + if err = txn.Put(db, []byte{2}, []byte{3}, 0); err != nil { + return err + } + _, v, err = cur.Get([]byte{2}, []byte{}, GetBothRange) + if err != nil { + panic(err) + } + if !bytes.Equal(v, []byte{1}) { + panic(v) + } + + return nil + }) + if err != nil { + t.Error(err) + } +} + // This test verifies the behavior of Cursor.Count when DupSort is provided. func TestCursor_Count_DupSort(t *testing.T) { env, _ := setup(t) diff --git a/mdbx/mdbxgo.c b/mdbx/mdbxgo.c index efe8588..5ba68a8 100644 --- a/mdbx/mdbxgo.c +++ b/mdbx/mdbxgo.c @@ -75,6 +75,11 @@ int mdbxgo_cursor_putmulti(MDBX_cursor *cur, char *kdata, size_t kn, char *vdata return mdbx_cursor_put(cur, &key, &val[0], flags); } +int mdbxgo_cursor_get01(MDBX_cursor *cur, char *vdata, size_t vn, MDBX_val *key, MDBX_val *val, MDBX_cursor_op op) { + MDBXGO_SET_VAL(val, vn, vdata); + return mdbx_cursor_get(cur, key, val, op); +} + int mdbxgo_cursor_get1(MDBX_cursor *cur, char *kdata, size_t kn, MDBX_val *key, MDBX_val *val, MDBX_cursor_op op) { MDBXGO_SET_VAL(key, kn, kdata); return mdbx_cursor_get(cur, key, val, op); diff --git a/mdbx/mdbxgo.h b/mdbx/mdbxgo.h index 79895fc..9b1ed64 100644 --- a/mdbx/mdbxgo.h +++ b/mdbx/mdbxgo.h @@ -24,6 +24,7 @@ int mdbxgo_put2(MDBX_txn *txn, MDBX_dbi dbi, char *kdata, size_t kn, char *vdata int mdbxgo_cursor_put1(MDBX_cursor *cur, char *kdata, size_t kn, MDBX_val *val, MDBX_put_flags_t flags); int mdbxgo_cursor_put2(MDBX_cursor *cur, char *kdata, size_t kn, char *vdata, size_t vn, MDBX_put_flags_t flags); int mdbxgo_cursor_putmulti(MDBX_cursor *cur, char *kdata, size_t kn, char *vdata, size_t vn, size_t vstride, MDBX_put_flags_t flags); +int mdbxgo_cursor_get01(MDBX_cursor *cur, char *vdata, size_t vn, MDBX_val *key, MDBX_val *val, MDBX_cursor_op op); int mdbxgo_cursor_get1(MDBX_cursor *cur, char *kdata, size_t kn, MDBX_val *key, MDBX_val *val, MDBX_cursor_op op); int mdbxgo_cursor_get2(MDBX_cursor *cur, char *kdata, size_t kn, char *vdata, size_t vn, MDBX_val *key, MDBX_val *val, MDBX_cursor_op op); /* ConstCString wraps a null-terminated (const char *) because Go's type system