Skip to content

Commit 2de3265

Browse files
mdbx: add Cursor.PutCurrent for DupSort in-place dup replacement (#208)
* mdbx: add Cursor.PutCurrent for DupSort in-place dup replacement For DupSort tables, after positioning with Get(GetBothRange), calling PutCurrent(key, newVal) replaces the current duplicate entry in-place using MDBX_CURRENT flag — saving one CGo call per update vs the previous Del(Current)+Put() pattern. Add TestCursor_PutCurrent_DupSort verifying the Erigon domain flush pattern: insert (key, stepPrefix+payload) pairs, seek to exact step, replace payload in-place, verify count and values are correct. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * mdbx: fix appendAssign lint in TestCursor_PutCurrent_DupSort Use explicit copies for val1/val2 to avoid appending to step1/step2 backing arrays (gocritic appendAssign). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * mdbx: simplify TestCursor_PutCurrent_DupSort — merge 4 txns into 2 Combine DBI open + insert into one transaction (matching the pattern used in other DupSort tests). Combine PutCurrent + verification into one transaction (read-your-own-writes within an Update works fine). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * go.mod: bump go directive to 1.24 for b.Loop() support BenchmarkCursor_Put_Sequence uses testing.Loop which requires go1.24. The toolchain was already go1.24.1; align the go directive to match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 010cf60 commit 2de3265

File tree

3 files changed

+149
-3
lines changed

3 files changed

+149
-3
lines changed

go.mod

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
module github.com/erigontech/mdbx-go
22

3-
go 1.23.0
4-
5-
toolchain go1.24.1
3+
go 1.24.0
64

75
require github.com/ianlancetaylor/cgosymbolizer v0.0.0-20241129212102-9c50ad6b591e
86

mdbx/cursor.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,16 @@ func (c *Cursor) PutMulti(key []byte, page []byte, stride int, flags uint) error
295295
return operrno("mdbxgo_cursor_putmulti", ret)
296296
}
297297

298+
// PutCurrent replaces the data of the item at the current cursor position.
299+
// For DupSort databases, this replaces the current duplicate entry in-place,
300+
// avoiding a separate Del+Put round-trip (saves one CGo call per update).
301+
// The cursor must be positioned (e.g. via Get with GetBothRange) before calling.
302+
//
303+
// Equivalent to Put(key, val, Current).
304+
func (c *Cursor) PutCurrent(key, val []byte) error {
305+
return c.Put(key, val, Current)
306+
}
307+
298308
// Del deletes the item referred to by the cursor from the database.
299309
//
300310
// See mdb_cursor_del.

mdbx/cursor_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1823,3 +1823,141 @@ func BenchmarkCursor_Set_Random(b *testing.B) {
18231823
b.Errorf("put: %v", err)
18241824
}
18251825
}
1826+
1827+
func BenchmarkCursor_Put_Sequence(b *testing.B) {
1828+
env, _ := setup(b)
1829+
1830+
const N = 100
1831+
var keys [N][]byte
1832+
for i := range keys {
1833+
keys[i] = make([]byte, 4)
1834+
binary.BigEndian.PutUint32(keys[i], uint32(i))
1835+
}
1836+
1837+
var db DBI
1838+
1839+
if err := env.Update(func(txn *Txn) (err error) {
1840+
db, err = txn.OpenRoot(0)
1841+
if err != nil {
1842+
return err
1843+
}
1844+
for i := range keys {
1845+
err = txn.Put(db, keys[i], keys[i], 0)
1846+
if err != nil {
1847+
return err
1848+
}
1849+
}
1850+
return nil
1851+
}); err != nil {
1852+
b.Errorf("dbi: %v", err)
1853+
return
1854+
}
1855+
1856+
if err := env.Update(func(txn *Txn) (err error) {
1857+
c, err := txn.OpenCursor(db)
1858+
if err != nil {
1859+
return err
1860+
}
1861+
b.ResetTimer()
1862+
for b.Loop() {
1863+
for i := 0; i < N; i++ {
1864+
if err = c.Put(keys[i], keys[i], 0); err != nil {
1865+
return err
1866+
}
1867+
}
1868+
}
1869+
return nil
1870+
}); err != nil {
1871+
b.Errorf("put: %v", err)
1872+
}
1873+
}
1874+
1875+
// TestCursor_PutCurrent_DupSort verifies that PutCurrent replaces the current
1876+
// duplicate entry in-place on a DupSort table, matching the pattern used in
1877+
// Erigon's DomainBufferedWriter.Flush:
1878+
//
1879+
// SeekBothRange(key, stepPrefix) → exact match → PutCurrent(key, newVal)
1880+
//
1881+
// This saves one CGo call vs the previous DeleteCurrent()+Put() approach.
1882+
func TestCursor_PutCurrent_DupSort(t *testing.T) {
1883+
env, _ := setup(t)
1884+
1885+
var db DBI
1886+
// Use an 8-byte step prefix (big-endian uint64) followed by a value payload,
1887+
// mirroring Erigon's domain value layout: stepBytes(8) || accountData.
1888+
step1 := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe} // ^uint64(1)
1889+
step2 := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd} // ^uint64(2)
1890+
1891+
val1 := append(append([]byte{}, step1...), []byte("balance=100")...)
1892+
val1v2 := append(append([]byte{}, step1...), []byte("balance=200")...) // same step, updated payload
1893+
val2 := append(append([]byte{}, step2...), []byte("balance=300")...)
1894+
1895+
key := []byte("account-address-1")
1896+
1897+
// Open DBI and insert two dup entries in one transaction.
1898+
if err := env.Update(func(txn *Txn) (err error) {
1899+
db, err = txn.OpenDBISimple("domains", Create|DupSort)
1900+
if err != nil {
1901+
return err
1902+
}
1903+
if err = txn.Put(db, key, val1, 0); err != nil {
1904+
return err
1905+
}
1906+
return txn.Put(db, key, val2, 0)
1907+
}); err != nil {
1908+
t.Fatal(err)
1909+
}
1910+
1911+
// PutCurrent to replace val1, then verify in the same transaction.
1912+
if err := env.Update(func(txn *Txn) error {
1913+
cur, err := txn.OpenCursor(db)
1914+
if err != nil {
1915+
return err
1916+
}
1917+
defer cur.Close()
1918+
1919+
// Position cursor on the dup entry whose value starts with step1.
1920+
_, foundVal, err := cur.Get(key, step1, GetBothRange)
1921+
if err != nil {
1922+
return err
1923+
}
1924+
if !bytes.HasPrefix(foundVal, step1) {
1925+
t.Fatalf("GetBothRange returned wrong dup: %x", foundVal)
1926+
}
1927+
1928+
// Replace in-place — one CGo call instead of Del(Current)+Put.
1929+
if err := cur.PutCurrent(key, val1v2); err != nil {
1930+
return err
1931+
}
1932+
1933+
// Verify count and values within the same transaction.
1934+
_, _, err = cur.Get(key, nil, Set)
1935+
if err != nil {
1936+
return err
1937+
}
1938+
count, err := cur.Count()
1939+
if err != nil {
1940+
return err
1941+
}
1942+
if count != 2 {
1943+
t.Errorf("expected 2 dups, got %d", count)
1944+
}
1945+
_, v, err := cur.Get(key, step1, GetBothRange)
1946+
if err != nil {
1947+
return err
1948+
}
1949+
if !bytes.Equal(v, val1v2) {
1950+
t.Errorf("first dup: expected %x, got %x", val1v2, v)
1951+
}
1952+
_, v, err = cur.Get(key, step2, GetBothRange)
1953+
if err != nil {
1954+
return err
1955+
}
1956+
if !bytes.Equal(v, val2) {
1957+
t.Errorf("second dup: expected %x, got %x", val2, v)
1958+
}
1959+
return nil
1960+
}); err != nil {
1961+
t.Fatal(err)
1962+
}
1963+
}

0 commit comments

Comments
 (0)