detached f361506

feat(tree): add persistent copy-on-write B-tree and snapshots

commit
f361506823ca149b1e727fa79ef3b9eb9296646e
author
brice · · 2026-10-10 01:45 UTC
parent
4338ab4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +861 -0
A snapshot.go +59 -0
@@ -0,0 +1,59 @@
1+// SPDX-License-Identifier: MIT
2+
3+package kv
4+
5+import "iter"
6+
7+// Item is the value stored under a key.
8+type Item struct {
9+ Value []byte // owned by the snapshot: must not be modified
10+ Version uint64 // seq of the batch that last wrote the key
11+}
12+
13+// Snapshot is an immutable, consistent view of the database at one seq. It is
14+// safe for concurrent use and stays valid, unchanged, after later writes.
15+// Keys yielded by iterators and Item.Value are owned by the snapshot and must
16+// not be modified.
17+type Snapshot struct {
18+ t tree
19+ seq uint64
20+}
21+
22+func newSnapshot(t tree, seq uint64) *Snapshot {
23+ return &Snapshot{t: t, seq: seq}
24+}
25+
26+// Seq returns the seq of the last batch applied in this snapshot.
27+func (s *Snapshot) Seq() uint64 { return s.seq }
28+
29+// Len returns the number of keys.
30+func (s *Snapshot) Len() int { return s.t.len() }
31+
32+// Get returns the item stored under key.
33+func (s *Snapshot) Get(key []byte) (Item, bool) { return s.t.get(key) }
34+
35+// Range iterates over the keys in [start, end) in ascending order. A nil end
36+// (or an empty start) is unbounded. Breaking out of the loop stops the walk.
37+func (s *Snapshot) Range(start, end []byte) iter.Seq2[[]byte, Item] {
38+ return s.t.ascend(start, end)
39+}
40+
41+// Prefix iterates over the keys that begin with p in ascending order. An
42+// empty p matches every key.
43+func (s *Snapshot) Prefix(p []byte) iter.Seq2[[]byte, Item] {
44+ return s.t.ascend(p, prefixEnd(p))
45+}
46+
47+// prefixEnd returns the smallest key greater than every key that begins with
48+// p, or nil if there is none (p is empty or all 0xFF).
49+func prefixEnd(p []byte) []byte {
50+ for i := len(p) - 1; i >= 0; i-- {
51+ if p[i] < 0xFF {
52+ end := make([]byte, i+1)
53+ copy(end, p)
54+ end[i]++
55+ return end
56+ }
57+ }
58+ return nil
59+}
@@ -0,0 +1,59 @@
1+// SPDX-License-Identifier: MIT
2+
3+package kv
4+
5+import "iter"
6+
7+// Item is the value stored under a key.
8+type Item struct {
9+ Value []byte // owned by the snapshot: must not be modified
10+ Version uint64 // seq of the batch that last wrote the key
11+}
12+
13+// Snapshot is an immutable, consistent view of the database at one seq. It is
14+// safe for concurrent use and stays valid, unchanged, after later writes.
15+// Keys yielded by iterators and Item.Value are owned by the snapshot and must
16+// not be modified.
17+type Snapshot struct {
18+ t tree
19+ seq uint64
20+}
21+
22+func newSnapshot(t tree, seq uint64) *Snapshot {
23+ return &Snapshot{t: t, seq: seq}
24+}
25+
26+// Seq returns the seq of the last batch applied in this snapshot.
27+func (s *Snapshot) Seq() uint64 { return s.seq }
28+
29+// Len returns the number of keys.
30+func (s *Snapshot) Len() int { return s.t.len() }
31+
32+// Get returns the item stored under key.
33+func (s *Snapshot) Get(key []byte) (Item, bool) { return s.t.get(key) }
34+
35+// Range iterates over the keys in [start, end) in ascending order. A nil end
36+// (or an empty start) is unbounded. Breaking out of the loop stops the walk.
37+func (s *Snapshot) Range(start, end []byte) iter.Seq2[[]byte, Item] {
38+ return s.t.ascend(start, end)
39+}
40+
41+// Prefix iterates over the keys that begin with p in ascending order. An
42+// empty p matches every key.
43+func (s *Snapshot) Prefix(p []byte) iter.Seq2[[]byte, Item] {
44+ return s.t.ascend(p, prefixEnd(p))
45+}
46+
47+// prefixEnd returns the smallest key greater than every key that begins with
48+// p, or nil if there is none (p is empty or all 0xFF).
49+func prefixEnd(p []byte) []byte {
50+ for i := len(p) - 1; i >= 0; i-- {
51+ if p[i] < 0xFF {
52+ end := make([]byte, i+1)
53+ copy(end, p)
54+ end[i]++
55+ return end
56+ }
57+ }
58+ return nil
59+}
A tree.go +356 -0
@@ -0,0 +1,356 @@
1+// SPDX-License-Identifier: MIT
2+
3+package kv
4+
5+import (
6+ "bytes"
7+ "iter"
8+ "slices"
9+)
10+
11+// The in-memory index is a persistent (copy-on-write) B-tree of order 32:
12+// a node holds at most maxEntries entries and, except for the root, at least
13+// minEntries. Entries live in internal nodes too.
14+const (
15+ degree = 32
16+ maxEntries = 2*degree - 1
17+ minEntries = degree - 1
18+)
19+
20+// entry is a key with its value and the seq of the batch that last wrote it.
21+// The tree never copies or modifies key and value: callers pass owned slices.
22+type entry struct {
23+ key []byte
24+ value []byte
25+ version uint64
26+}
27+
28+// owner identifies one builder epoch. It has a non-zero size so that every
29+// new(owner) is a distinct pointer.
30+type owner struct{ _ byte }
31+
32+// node is a B-tree node. A node without children is a leaf; otherwise it has
33+// len(entries)+1 children.
34+//
35+// Invariant: a node is mutated only by the builder whose current owner token
36+// equals node.own. freeze gives the builder a fresh token, so no live builder
37+// holds the token of any node reachable from a frozen tree: published nodes
38+// are never mutated, and a builder clones a node the first time it touches it.
39+type node struct {
40+ own *owner
41+ entries []entry
42+ children []*node
43+}
44+
45+func (n *node) leaf() bool { return len(n.children) == 0 }
46+
47+// find returns the index of the first entry whose key is >= key and whether
48+// that entry's key equals key.
49+func (n *node) find(key []byte) (int, bool) {
50+ return slices.BinarySearchFunc(n.entries, key, func(e entry, k []byte) int {
51+ return bytes.Compare(e.key, k)
52+ })
53+}
54+
55+// lookup returns the entry stored under key, if any.
56+func lookup(n *node, key []byte) (entry, bool) {
57+ for n != nil {
58+ i, found := n.find(key)
59+ if found {
60+ return n.entries[i], true
61+ }
62+ if n.leaf() {
63+ break
64+ }
65+ n = n.children[i]
66+ }
67+ return entry{}, false
68+}
69+
70+// tree is an immutable persistent B-tree; the zero value is empty. Its methods
71+// never modify existing nodes, so a tree can be shared between goroutines.
72+type tree struct {
73+ root *node
74+ n int
75+}
76+
77+func (t tree) len() int { return t.n }
78+
79+func (t tree) get(key []byte) (Item, bool) {
80+ e, ok := lookup(t.root, key)
81+ return Item{Value: e.value, Version: e.version}, ok
82+}
83+
84+// put returns a tree where key maps to value at version; t is untouched.
85+func (t tree) put(key, value []byte, version uint64) tree {
86+ b := t.builder()
87+ b.put(key, value, version)
88+ return b.freeze()
89+}
90+
91+// delete returns a tree without key; t is untouched.
92+func (t tree) delete(key []byte) tree {
93+ b := t.builder()
94+ b.delete(key)
95+ return b.freeze()
96+}
97+
98+// ascend yields the entries with start <= key < end in ascending order. An
99+// empty start and a nil end are unbounded. It walks with an explicit stack and
100+// does not allocate per yielded item.
101+func (t tree) ascend(start, end []byte) iter.Seq2[[]byte, Item] {
102+ return func(yield func([]byte, Item) bool) {
103+ // A frame's i is the index of the next entry of n to yield; the
104+ // child left of that entry has already been visited or skipped.
105+ type frame struct {
106+ n *node
107+ i int
108+ }
109+ var buf [16]frame
110+ stack := buf[:0]
111+
112+ // Seek to the first entry >= start.
113+ for n := t.root; n != nil; {
114+ i, found := n.find(start)
115+ stack = append(stack, frame{n, i})
116+ if found || n.leaf() {
117+ break
118+ }
119+ n = n.children[i]
120+ }
121+
122+ for len(stack) > 0 {
123+ f := &stack[len(stack)-1]
124+ if f.i >= len(f.n.entries) {
125+ stack = stack[:len(stack)-1]
126+ continue
127+ }
128+ n := f.n
129+ e := &n.entries[f.i]
130+ f.i++
131+ if end != nil && bytes.Compare(e.key, end) >= 0 {
132+ return
133+ }
134+ if !yield(e.key, Item{Value: e.value, Version: e.version}) {
135+ return
136+ }
137+ // Descend to the leftmost leaf of the subtree right of e.
138+ for c := n; !c.leaf(); {
139+ c = c.children[f.i]
140+ stack = append(stack, frame{c, 0})
141+ f = &stack[len(stack)-1]
142+ }
143+ }
144+ }
145+}
146+
147+// builder applies many mutations to a tree while copying each touched node at
148+// most once. It is not safe for concurrent use. Trees returned by freeze are
149+// immutable and independent of later builder operations.
150+type builder struct {
151+ root *node
152+ n int
153+ own *owner
154+}
155+
156+// builder returns a builder starting from t; t is untouched.
157+func (t tree) builder() *builder {
158+ return &builder{root: t.root, n: t.n, own: new(owner)}
159+}
160+
161+// freeze returns the current state as an immutable tree. The builder remains
162+// usable: it switches to a new owner token, so nodes of the returned tree are
163+// cloned before any further mutation.
164+func (b *builder) freeze() tree {
165+ b.own = new(owner)
166+ return tree{root: b.root, n: b.n}
167+}
168+
169+func (b *builder) len() int { return b.n }
170+
171+func (b *builder) get(key []byte) (Item, bool) {
172+ e, ok := lookup(b.root, key)
173+ return Item{Value: e.value, Version: e.version}, ok
174+}
175+
176+// mutable returns n if this builder owns it, else an owned shallow copy.
177+func (b *builder) mutable(n *node) *node {
178+ if n.own == b.own {
179+ return n
180+ }
181+ c := &node{own: b.own, entries: slices.Clone(n.entries)}
182+ if len(n.children) > 0 {
183+ c.children = slices.Clone(n.children)
184+ }
185+ return c
186+}
187+
188+// mutableChild makes n.children[i] owned and returns it. n must be owned.
189+func (b *builder) mutableChild(n *node, i int) *node {
190+ c := b.mutable(n.children[i])
191+ n.children[i] = c
192+ return c
193+}
194+
195+// split cuts the owned node n at entry i: n keeps entries[:i], the returned
196+// entry is entries[i], and the returned node holds the rest.
197+func (b *builder) split(n *node, i int) (entry, *node) {
198+ mid := n.entries[i]
199+ r := &node{own: b.own, entries: slices.Clone(n.entries[i+1:])}
200+ clear(n.entries[i:])
201+ n.entries = n.entries[:i]
202+ if !n.leaf() {
203+ r.children = slices.Clone(n.children[i+1:])
204+ clear(n.children[i+1:])
205+ n.children = n.children[:i+1]
206+ }
207+ return mid, r
208+}
209+
210+// put sets key to value at version, replacing any existing entry.
211+func (b *builder) put(key, value []byte, version uint64) {
212+ e := entry{key: key, value: value, version: version}
213+ if b.root == nil {
214+ b.root = &node{own: b.own, entries: []entry{e}}
215+ b.n = 1
216+ return
217+ }
218+ b.root = b.mutable(b.root)
219+ if len(b.root.entries) >= maxEntries {
220+ mid, r := b.split(b.root, maxEntries/2)
221+ b.root = &node{
222+ own: b.own,
223+ entries: []entry{mid},
224+ children: []*node{b.root, r},
225+ }
226+ }
227+ if !b.insert(b.root, e) {
228+ b.n++
229+ }
230+}
231+
232+// insert adds e below the owned, non-full node n and reports whether it
233+// replaced an existing entry. Full children are split on the way down.
234+func (b *builder) insert(n *node, e entry) (replaced bool) {
235+ for {
236+ i, found := n.find(e.key)
237+ if found {
238+ n.entries[i] = e
239+ return true
240+ }
241+ if n.leaf() {
242+ n.entries = slices.Insert(n.entries, i, e)
243+ return false
244+ }
245+ c := b.mutableChild(n, i)
246+ if len(c.entries) >= maxEntries {
247+ mid, r := b.split(c, maxEntries/2)
248+ n.entries = slices.Insert(n.entries, i, mid)
249+ n.children = slices.Insert(n.children, i+1, r)
250+ switch cmp := bytes.Compare(e.key, mid.key); {
251+ case cmp == 0:
252+ n.entries[i] = e
253+ return true
254+ case cmp > 0:
255+ c = r
256+ }
257+ }
258+ n = c
259+ }
260+}
261+
262+// delete removes key if present.
263+func (b *builder) delete(key []byte) {
264+ if _, ok := lookup(b.root, key); !ok {
265+ return
266+ }
267+ b.root = b.mutable(b.root)
268+ b.remove(b.root, key, false)
269+ b.n--
270+ switch {
271+ case len(b.root.entries) > 0:
272+ case b.root.leaf():
273+ b.root = nil
274+ default:
275+ b.root = b.root.children[0]
276+ }
277+}
278+
279+// remove deletes key (or, with max, the largest entry) from the subtree of the
280+// owned node n and returns the removed entry. The key must exist. Every child
281+// descended into is first guaranteed to hold more than minEntries entries.
282+func (b *builder) remove(n *node, key []byte, max bool) entry {
283+ for {
284+ var i int
285+ var found bool
286+ switch {
287+ case max && n.leaf():
288+ i, found = len(n.entries)-1, true
289+ case max:
290+ i = len(n.entries)
291+ default:
292+ i, found = n.find(key)
293+ }
294+ if n.leaf() {
295+ e := n.entries[i]
296+ n.entries = slices.Delete(n.entries, i, i+1)
297+ return e
298+ }
299+ if len(n.children[i].entries) <= minEntries {
300+ b.growChild(n, i)
301+ continue // the shape of n changed: search again
302+ }
303+ c := b.mutableChild(n, i)
304+ if found {
305+ // Replace the entry by its predecessor.
306+ e := n.entries[i]
307+ n.entries[i] = b.remove(c, nil, true)
308+ return e
309+ }
310+ n = c
311+ }
312+}
313+
314+// growChild ensures n.children[i] has more than minEntries entries by
315+// borrowing from a sibling or merging with one. n must be owned.
316+func (b *builder) growChild(n *node, i int) {
317+ switch {
318+ case i > 0 && len(n.children[i-1].entries) > minEntries:
319+ c := b.mutableChild(n, i)
320+ l := b.mutableChild(n, i-1)
321+ last := len(l.entries) - 1
322+ c.entries = slices.Insert(c.entries, 0, n.entries[i-1])
323+ n.entries[i-1] = l.entries[last]
324+ l.entries[last] = entry{}
325+ l.entries = l.entries[:last]
326+ if !c.leaf() {
327+ lc := len(l.children) - 1
328+ c.children = slices.Insert(c.children, 0, l.children[lc])
329+ l.children[lc] = nil
330+ l.children = l.children[:lc]
331+ }
332+ case i < len(n.entries) && len(n.children[i+1].entries) > minEntries:
333+ c := b.mutableChild(n, i)
334+ r := b.mutableChild(n, i+1)
335+ c.entries = append(c.entries, n.entries[i])
336+ n.entries[i] = r.entries[0]
337+ r.entries = slices.Delete(r.entries, 0, 1)
338+ if !c.leaf() {
339+ c.children = append(c.children, r.children[0])
340+ r.children = slices.Delete(r.children, 0, 1)
341+ }
342+ default:
343+ // Merge children i and i+1 around entry i (children i-1 and i
344+ // around entry i-1 for the last child).
345+ if i == len(n.entries) {
346+ i--
347+ }
348+ c := b.mutableChild(n, i)
349+ r := n.children[i+1] // only read
350+ c.entries = append(c.entries, n.entries[i])
351+ c.entries = append(c.entries, r.entries...)
352+ c.children = append(c.children, r.children...)
353+ n.entries = slices.Delete(n.entries, i, i+1)
354+ n.children = slices.Delete(n.children, i+1, i+2)
355+ }
356+}
@@ -0,0 +1,356 @@
1+// SPDX-License-Identifier: MIT
2+
3+package kv
4+
5+import (
6+ "bytes"
7+ "iter"
8+ "slices"
9+)
10+
11+// The in-memory index is a persistent (copy-on-write) B-tree of order 32:
12+// a node holds at most maxEntries entries and, except for the root, at least
13+// minEntries. Entries live in internal nodes too.
14+const (
15+ degree = 32
16+ maxEntries = 2*degree - 1
17+ minEntries = degree - 1
18+)
19+
20+// entry is a key with its value and the seq of the batch that last wrote it.
21+// The tree never copies or modifies key and value: callers pass owned slices.
22+type entry struct {
23+ key []byte
24+ value []byte
25+ version uint64
26+}
27+
28+// owner identifies one builder epoch. It has a non-zero size so that every
29+// new(owner) is a distinct pointer.
30+type owner struct{ _ byte }
31+
32+// node is a B-tree node. A node without children is a leaf; otherwise it has
33+// len(entries)+1 children.
34+//
35+// Invariant: a node is mutated only by the builder whose current owner token
36+// equals node.own. freeze gives the builder a fresh token, so no live builder
37+// holds the token of any node reachable from a frozen tree: published nodes
38+// are never mutated, and a builder clones a node the first time it touches it.
39+type node struct {
40+ own *owner
41+ entries []entry
42+ children []*node
43+}
44+
45+func (n *node) leaf() bool { return len(n.children) == 0 }
46+
47+// find returns the index of the first entry whose key is >= key and whether
48+// that entry's key equals key.
49+func (n *node) find(key []byte) (int, bool) {
50+ return slices.BinarySearchFunc(n.entries, key, func(e entry, k []byte) int {
51+ return bytes.Compare(e.key, k)
52+ })
53+}
54+
55+// lookup returns the entry stored under key, if any.
56+func lookup(n *node, key []byte) (entry, bool) {
57+ for n != nil {
58+ i, found := n.find(key)
59+ if found {
60+ return n.entries[i], true
61+ }
62+ if n.leaf() {
63+ break
64+ }
65+ n = n.children[i]
66+ }
67+ return entry{}, false
68+}
69+
70+// tree is an immutable persistent B-tree; the zero value is empty. Its methods
71+// never modify existing nodes, so a tree can be shared between goroutines.
72+type tree struct {
73+ root *node
74+ n int
75+}
76+
77+func (t tree) len() int { return t.n }
78+
79+func (t tree) get(key []byte) (Item, bool) {
80+ e, ok := lookup(t.root, key)
81+ return Item{Value: e.value, Version: e.version}, ok
82+}
83+
84+// put returns a tree where key maps to value at version; t is untouched.
85+func (t tree) put(key, value []byte, version uint64) tree {
86+ b := t.builder()
87+ b.put(key, value, version)
88+ return b.freeze()
89+}
90+
91+// delete returns a tree without key; t is untouched.
92+func (t tree) delete(key []byte) tree {
93+ b := t.builder()
94+ b.delete(key)
95+ return b.freeze()
96+}
97+
98+// ascend yields the entries with start <= key < end in ascending order. An
99+// empty start and a nil end are unbounded. It walks with an explicit stack and
100+// does not allocate per yielded item.
101+func (t tree) ascend(start, end []byte) iter.Seq2[[]byte, Item] {
102+ return func(yield func([]byte, Item) bool) {
103+ // A frame's i is the index of the next entry of n to yield; the
104+ // child left of that entry has already been visited or skipped.
105+ type frame struct {
106+ n *node
107+ i int
108+ }
109+ var buf [16]frame
110+ stack := buf[:0]
111+
112+ // Seek to the first entry >= start.
113+ for n := t.root; n != nil; {
114+ i, found := n.find(start)
115+ stack = append(stack, frame{n, i})
116+ if found || n.leaf() {
117+ break
118+ }
119+ n = n.children[i]
120+ }
121+
122+ for len(stack) > 0 {
123+ f := &stack[len(stack)-1]
124+ if f.i >= len(f.n.entries) {
125+ stack = stack[:len(stack)-1]
126+ continue
127+ }
128+ n := f.n
129+ e := &n.entries[f.i]
130+ f.i++
131+ if end != nil && bytes.Compare(e.key, end) >= 0 {
132+ return
133+ }
134+ if !yield(e.key, Item{Value: e.value, Version: e.version}) {
135+ return
136+ }
137+ // Descend to the leftmost leaf of the subtree right of e.
138+ for c := n; !c.leaf(); {
139+ c = c.children[f.i]
140+ stack = append(stack, frame{c, 0})
141+ f = &stack[len(stack)-1]
142+ }
143+ }
144+ }
145+}
146+
147+// builder applies many mutations to a tree while copying each touched node at
148+// most once. It is not safe for concurrent use. Trees returned by freeze are
149+// immutable and independent of later builder operations.
150+type builder struct {
151+ root *node
152+ n int
153+ own *owner
154+}
155+
156+// builder returns a builder starting from t; t is untouched.
157+func (t tree) builder() *builder {
158+ return &builder{root: t.root, n: t.n, own: new(owner)}
159+}
160+
161+// freeze returns the current state as an immutable tree. The builder remains
162+// usable: it switches to a new owner token, so nodes of the returned tree are
163+// cloned before any further mutation.
164+func (b *builder) freeze() tree {
165+ b.own = new(owner)
166+ return tree{root: b.root, n: b.n}
167+}
168+
169+func (b *builder) len() int { return b.n }
170+
171+func (b *builder) get(key []byte) (Item, bool) {
172+ e, ok := lookup(b.root, key)
173+ return Item{Value: e.value, Version: e.version}, ok
174+}
175+
176+// mutable returns n if this builder owns it, else an owned shallow copy.
177+func (b *builder) mutable(n *node) *node {
178+ if n.own == b.own {
179+ return n
180+ }
181+ c := &node{own: b.own, entries: slices.Clone(n.entries)}
182+ if len(n.children) > 0 {
183+ c.children = slices.Clone(n.children)
184+ }
185+ return c
186+}
187+
188+// mutableChild makes n.children[i] owned and returns it. n must be owned.
189+func (b *builder) mutableChild(n *node, i int) *node {
190+ c := b.mutable(n.children[i])
191+ n.children[i] = c
192+ return c
193+}
194+
195+// split cuts the owned node n at entry i: n keeps entries[:i], the returned
196+// entry is entries[i], and the returned node holds the rest.
197+func (b *builder) split(n *node, i int) (entry, *node) {
198+ mid := n.entries[i]
199+ r := &node{own: b.own, entries: slices.Clone(n.entries[i+1:])}
200+ clear(n.entries[i:])
201+ n.entries = n.entries[:i]
202+ if !n.leaf() {
203+ r.children = slices.Clone(n.children[i+1:])
204+ clear(n.children[i+1:])
205+ n.children = n.children[:i+1]
206+ }
207+ return mid, r
208+}
209+
210+// put sets key to value at version, replacing any existing entry.
211+func (b *builder) put(key, value []byte, version uint64) {
212+ e := entry{key: key, value: value, version: version}
213+ if b.root == nil {
214+ b.root = &node{own: b.own, entries: []entry{e}}
215+ b.n = 1
216+ return
217+ }
218+ b.root = b.mutable(b.root)
219+ if len(b.root.entries) >= maxEntries {
220+ mid, r := b.split(b.root, maxEntries/2)
221+ b.root = &node{
222+ own: b.own,
223+ entries: []entry{mid},
224+ children: []*node{b.root, r},
225+ }
226+ }
227+ if !b.insert(b.root, e) {
228+ b.n++
229+ }
230+}
231+
232+// insert adds e below the owned, non-full node n and reports whether it
233+// replaced an existing entry. Full children are split on the way down.
234+func (b *builder) insert(n *node, e entry) (replaced bool) {
235+ for {
236+ i, found := n.find(e.key)
237+ if found {
238+ n.entries[i] = e
239+ return true
240+ }
241+ if n.leaf() {
242+ n.entries = slices.Insert(n.entries, i, e)
243+ return false
244+ }
245+ c := b.mutableChild(n, i)
246+ if len(c.entries) >= maxEntries {
247+ mid, r := b.split(c, maxEntries/2)
248+ n.entries = slices.Insert(n.entries, i, mid)
249+ n.children = slices.Insert(n.children, i+1, r)
250+ switch cmp := bytes.Compare(e.key, mid.key); {
251+ case cmp == 0:
252+ n.entries[i] = e
253+ return true
254+ case cmp > 0:
255+ c = r
256+ }
257+ }
258+ n = c
259+ }
260+}
261+
262+// delete removes key if present.
263+func (b *builder) delete(key []byte) {
264+ if _, ok := lookup(b.root, key); !ok {
265+ return
266+ }
267+ b.root = b.mutable(b.root)
268+ b.remove(b.root, key, false)
269+ b.n--
270+ switch {
271+ case len(b.root.entries) > 0:
272+ case b.root.leaf():
273+ b.root = nil
274+ default:
275+ b.root = b.root.children[0]
276+ }
277+}
278+
279+// remove deletes key (or, with max, the largest entry) from the subtree of the
280+// owned node n and returns the removed entry. The key must exist. Every child
281+// descended into is first guaranteed to hold more than minEntries entries.
282+func (b *builder) remove(n *node, key []byte, max bool) entry {
283+ for {
284+ var i int
285+ var found bool
286+ switch {
287+ case max && n.leaf():
288+ i, found = len(n.entries)-1, true
289+ case max:
290+ i = len(n.entries)
291+ default:
292+ i, found = n.find(key)
293+ }
294+ if n.leaf() {
295+ e := n.entries[i]
296+ n.entries = slices.Delete(n.entries, i, i+1)
297+ return e
298+ }
299+ if len(n.children[i].entries) <= minEntries {
300+ b.growChild(n, i)
301+ continue // the shape of n changed: search again
302+ }
303+ c := b.mutableChild(n, i)
304+ if found {
305+ // Replace the entry by its predecessor.
306+ e := n.entries[i]
307+ n.entries[i] = b.remove(c, nil, true)
308+ return e
309+ }
310+ n = c
311+ }
312+}
313+
314+// growChild ensures n.children[i] has more than minEntries entries by
315+// borrowing from a sibling or merging with one. n must be owned.
316+func (b *builder) growChild(n *node, i int) {
317+ switch {
318+ case i > 0 && len(n.children[i-1].entries) > minEntries:
319+ c := b.mutableChild(n, i)
320+ l := b.mutableChild(n, i-1)
321+ last := len(l.entries) - 1
322+ c.entries = slices.Insert(c.entries, 0, n.entries[i-1])
323+ n.entries[i-1] = l.entries[last]
324+ l.entries[last] = entry{}
325+ l.entries = l.entries[:last]
326+ if !c.leaf() {
327+ lc := len(l.children) - 1
328+ c.children = slices.Insert(c.children, 0, l.children[lc])
329+ l.children[lc] = nil
330+ l.children = l.children[:lc]
331+ }
332+ case i < len(n.entries) && len(n.children[i+1].entries) > minEntries:
333+ c := b.mutableChild(n, i)
334+ r := b.mutableChild(n, i+1)
335+ c.entries = append(c.entries, n.entries[i])
336+ n.entries[i] = r.entries[0]
337+ r.entries = slices.Delete(r.entries, 0, 1)
338+ if !c.leaf() {
339+ c.children = append(c.children, r.children[0])
340+ r.children = slices.Delete(r.children, 0, 1)
341+ }
342+ default:
343+ // Merge children i and i+1 around entry i (children i-1 and i
344+ // around entry i-1 for the last child).
345+ if i == len(n.entries) {
346+ i--
347+ }
348+ c := b.mutableChild(n, i)
349+ r := n.children[i+1] // only read
350+ c.entries = append(c.entries, n.entries[i])
351+ c.entries = append(c.entries, r.entries...)
352+ c.children = append(c.children, r.children...)
353+ n.entries = slices.Delete(n.entries, i, i+1)
354+ n.children = slices.Delete(n.children, i+1, i+2)
355+ }
356+}
A tree_test.go +446 -0
@@ -0,0 +1,446 @@
1+// SPDX-License-Identifier: MIT
2+
3+package kv
4+
5+import (
6+ "bytes"
7+ "fmt"
8+ "math/rand/v2"
9+ "slices"
10+ "testing"
11+)
12+
13+// model is the reference implementation: a map plus a sort on demand.
14+type model map[string]Item
15+
16+func (m model) clone() model {
17+ c := make(model, len(m))
18+ for k, v := range m {
19+ c[k] = v
20+ }
21+ return c
22+}
23+
24+// sorted returns the keys of m in ascending order.
25+func (m model) sorted() []string {
26+ ks := make([]string, 0, len(m))
27+ for k := range m {
28+ ks = append(ks, k)
29+ }
30+ slices.Sort(ks)
31+ return ks
32+}
33+
34+// within returns the part of the sorted keys in [start, end); nil end = unbounded.
35+func within(sorted []string, start, end []byte) []string {
36+ lo, _ := slices.BinarySearch(sorted, string(start))
37+ hi := len(sorted)
38+ if end != nil {
39+ hi, _ = slices.BinarySearch(sorted, string(end))
40+ }
41+ if hi < lo {
42+ return nil
43+ }
44+ return sorted[lo:hi]
45+}
46+
47+type kvPair struct {
48+ key string
49+ item Item
50+}
51+
52+func collect(seq func(func([]byte, Item) bool)) []kvPair {
53+ var out []kvPair
54+ seq(func(k []byte, it Item) bool {
55+ out = append(out, kvPair{string(k), it})
56+ return true
57+ })
58+ return out
59+}
60+
61+func equalItem(a, b Item) bool {
62+ return a.Version == b.Version && bytes.Equal(a.Value, b.Value)
63+}
64+
65+// checkStructure verifies the B-tree invariants: ordering, fill bounds,
66+// uniform leaf depth and the entry count.
67+func checkStructure(t *testing.T, tr tree) {
68+ t.Helper()
69+ if tr.root == nil {
70+ if tr.n != 0 {
71+ t.Fatalf("empty root but n=%d", tr.n)
72+ }
73+ return
74+ }
75+ var count, leafDepth int
76+ var prev []byte
77+ var walk func(n *node, depth int, isRoot bool)
78+ walk = func(n *node, depth int, isRoot bool) {
79+ if len(n.entries) == 0 || len(n.entries) > maxEntries || (!isRoot && len(n.entries) < minEntries) {
80+ t.Fatalf("node with %d entries at depth %d", len(n.entries), depth)
81+ }
82+ if n.leaf() {
83+ if leafDepth == 0 {
84+ leafDepth = depth
85+ } else if leafDepth != depth {
86+ t.Fatalf("leaves at depths %d and %d", leafDepth, depth)
87+ }
88+ } else if len(n.children) != len(n.entries)+1 {
89+ t.Fatalf("%d entries but %d children", len(n.entries), len(n.children))
90+ }
91+ for i, e := range n.entries {
92+ if !n.leaf() {
93+ walk(n.children[i], depth+1, false)
94+ }
95+ if prev != nil && bytes.Compare(prev, e.key) >= 0 {
96+ t.Fatalf("keys out of order: %x then %x", prev, e.key)
97+ }
98+ prev = e.key
99+ count++
100+ }
101+ if !n.leaf() {
102+ walk(n.children[len(n.entries)], depth+1, false)
103+ }
104+ }
105+ walk(tr.root, 1, true)
106+ if count != tr.n {
107+ t.Fatalf("tree has %d entries, n=%d", count, tr.n)
108+ }
109+}
110+
111+// checkAgainst compares the tree with the model through get, full scan,
112+// random ranges and random prefixes.
113+func checkAgainst(t *testing.T, rng *rand.Rand, tr tree, m model, genKey func() []byte) {
114+ t.Helper()
115+ checkStructure(t, tr)
116+ if tr.len() != len(m) {
117+ t.Fatalf("len = %d, want %d", tr.len(), len(m))
118+ }
119+ s := newSnapshot(tr, 7)
120+ if s.Len() != len(m) || s.Seq() != 7 {
121+ t.Fatalf("snapshot Len/Seq = %d/%d", s.Len(), s.Seq())
122+ }
123+ sorted := m.sorted()
124+ verify := func(what string, got []kvPair, start, end []byte) {
125+ t.Helper()
126+ want := within(sorted, start, end)
127+ if len(got) != len(want) {
128+ t.Fatalf("%s: got %d items, want %d", what, len(got), len(want))
129+ }
130+ for i, k := range want {
131+ if got[i].key != k || !equalItem(got[i].item, m[k]) {
132+ t.Fatalf("%s: item %d = %x, want %x", what, i, got[i].key, k)
133+ }
134+ }
135+ }
136+ verify("full scan", collect(s.Range(nil, nil)), nil, nil)
137+
138+ for range 20 {
139+ k := genKey()
140+ got, ok := s.Get(k)
141+ want, wok := m[string(k)]
142+ if ok != wok || (ok && !equalItem(got, want)) {
143+ t.Fatalf("Get(%x) = %v,%v want %v,%v", k, got, ok, want, wok)
144+ }
145+ }
146+ for k, want := range m {
147+ if got, ok := s.Get([]byte(k)); !ok || !equalItem(got, want) {
148+ t.Fatalf("Get(%x) = %v,%v want %v", k, got, ok, want)
149+ }
150+ break // one arbitrary present key is enough per check
151+ }
152+ for range 10 {
153+ a, b := genKey(), genKey()
154+ if bytes.Compare(a, b) > 0 {
155+ a, b = b, a
156+ }
157+ verify("range", collect(s.Range(a, b)), a, b)
158+ verify("range open end", collect(s.Range(a, nil)), a, nil)
159+ verify("range open start", collect(s.Range(nil, b)), nil, b)
160+ }
161+ for range 10 {
162+ p := genKey()
163+ p = p[:rng.IntN(len(p)+1)] // may be empty
164+ var want []string
165+ for _, k := range sorted {
166+ if bytes.HasPrefix([]byte(k), p) {
167+ want = append(want, k)
168+ }
169+ }
170+ got := collect(s.Prefix(p))
171+ if len(got) != len(want) {
172+ t.Fatalf("prefix %x: got %d items, want %d", p, len(got), len(want))
173+ }
174+ for i, k := range want {
175+ if got[i].key != k {
176+ t.Fatalf("prefix %x: item %d = %x, want %x", p, i, got[i].key, k)
177+ }
178+ }
179+ }
180+ // Early break stops the walk.
181+ seen := 0
182+ for range s.Range(nil, nil) {
183+ seen++
184+ if seen == 3 {
185+ break
186+ }
187+ }
188+ if seen != min(3, len(m)) {
189+ t.Fatalf("early break saw %d items", seen)
190+ }
191+}
192+
193+type frozen struct {
194+ tr tree
195+ m model
196+}
197+
198+func TestTreeModel(t *testing.T) {
199+ seeds, ops := 50, 10000
200+ if testing.Short() {
201+ seeds, ops = 8, 2000
202+ }
203+ alphabet := []byte{0, 1, 2, 'a', 'b', 'c', 0x7F, 0x80, 0xFD, 0xFE, 0xFF, 'x', 'y', 'z', 0x10, 0x20}
204+ for seed := range seeds {
205+ t.Run(fmt.Sprintf("seed=%d", seed), func(t *testing.T) {
206+ rng := rand.New(rand.NewPCG(uint64(seed), 0xe131))
207+ alpha := alphabet[:2+seed%(len(alphabet)-1)]
208+ genKey := func() []byte {
209+ k := make([]byte, 1+rng.IntN(5))
210+ for i := range k {
211+ k[i] = alpha[rng.IntN(len(alpha))]
212+ }
213+ return k
214+ }
215+ var (
216+ tr tree
217+ m = model{}
218+ version uint64
219+ history []frozen
220+ )
221+ apply := func(del bool, k []byte, put func(k, v []byte, ver uint64), rm func(k []byte)) {
222+ version++
223+ if del {
224+ rm(k)
225+ delete(m, string(k))
226+ return
227+ }
228+ v := make([]byte, rng.IntN(8))
229+ for i := range v {
230+ v[i] = byte(rng.IntN(256))
231+ }
232+ put(k, v, version)
233+ m[string(k)] = Item{Value: v, Version: version}
234+ }
235+ for done, next := 0, 0; done < ops; {
236+ if rng.IntN(5) == 0 {
237+ // A group of ops through one builder.
238+ b := tr.builder()
239+ n := 1 + rng.IntN(40)
240+ for range n {
241+ k := genKey()
242+ apply(rng.IntN(3) == 0, k, b.put, b.delete)
243+ if got, ok := b.get(k); ok != (m[string(k)].Version != 0) || (ok && !equalItem(got, m[string(k)])) {
244+ t.Fatalf("seed %d: builder.get mismatch", seed)
245+ }
246+ }
247+ if b.len() != len(m) {
248+ t.Fatalf("seed %d: builder.len = %d, want %d", seed, b.len(), len(m))
249+ }
250+ tr = b.freeze()
251+ done += n
252+ } else {
253+ k := genKey()
254+ apply(rng.IntN(3) == 0, k, func(k, v []byte, ver uint64) { tr = tr.put(k, v, ver) }, func(k []byte) { tr = tr.delete(k) })
255+ done++
256+ }
257+ if done >= next {
258+ next = done + 500 + rng.IntN(500)
259+ checkAgainst(t, rng, tr, m, genKey)
260+ history = append(history, frozen{tr, m.clone()})
261+ }
262+ }
263+ checkAgainst(t, rng, tr, m, genKey)
264+ // Persistence: every older tree still equals its model.
265+ for _, h := range history {
266+ checkAgainst(t, rng, h.tr, h.m, genKey)
267+ }
268+ // Drain the tree in random order, with and without a builder.
269+ keys := m.sorted()
270+ rng.Shuffle(len(keys), func(i, j int) { keys[i], keys[j] = keys[j], keys[i] })
271+ for i, k := range keys {
272+ if i%2 == 0 {
273+ tr = tr.delete([]byte(k))
274+ } else {
275+ b := tr.builder()
276+ b.delete([]byte(k))
277+ b.delete([]byte(k)) // absent: no-op
278+ tr = b.freeze()
279+ }
280+ delete(m, k)
281+ if i%97 == 0 {
282+ checkStructure(t, tr)
283+ }
284+ }
285+ checkAgainst(t, rng, tr, m, genKey)
286+ if tr.root != nil || tr.len() != 0 {
287+ t.Fatalf("seed %d: tree not empty after draining", seed)
288+ }
289+ // Draining must not have disturbed the older trees.
290+ for _, h := range history {
291+ checkAgainst(t, rng, h.tr, h.m, genKey)
292+ }
293+ })
294+ }
295+}
296+
297+// TestBuilderReuse checks that nodes of a frozen tree are never mutated by
298+// later builder operations, and that nodes owned by the builder are reused.
299+func TestBuilderReuse(t *testing.T) {
300+ var base tree
301+ for i := range 500 {
302+ base = base.put(fmt.Appendf(nil, "k%04d", i), []byte("v"), 1)
303+ }
304+ b := base.builder()
305+ b.put([]byte("k0100"), []byte("new"), 2)
306+ first := b.root
307+ b.put([]byte("k0101"), []byte("new"), 2)
308+ if b.root != first {
309+ t.Fatal("builder copied its own root twice")
310+ }
311+ frozenTree := b.freeze()
312+ b.put([]byte("k0102"), []byte("again"), 3)
313+ if b.root == frozenTree.root {
314+ t.Fatal("builder mutated the root of a frozen tree")
315+ }
316+ if it, _ := frozenTree.get([]byte("k0102")); string(it.Value) != "v" {
317+ t.Fatalf("frozen tree changed: %q", it.Value)
318+ }
319+ if it, _ := base.get([]byte("k0100")); string(it.Value) != "v" || it.Version != 1 {
320+ t.Fatalf("base tree changed: %v", it)
321+ }
322+ checkStructure(t, base)
323+ checkStructure(t, frozenTree)
324+ checkStructure(t, b.freeze())
325+}
326+
327+func TestPrefixEnd(t *testing.T) {
328+ tests := []struct{ in, want []byte }{
329+ {nil, nil},
330+ {[]byte{}, nil},
331+ {[]byte("a"), []byte("b")},
332+ {[]byte("ab"), []byte("ac")},
333+ {[]byte{'a', 0xFF}, []byte("b")},
334+ {[]byte{'a', 0xFF, 0xFF}, []byte("b")},
335+ {[]byte{0xFF}, nil},
336+ {[]byte{0xFF, 0xFF}, nil},
337+ }
338+ for _, tt := range tests {
339+ if got := prefixEnd(tt.in); !bytes.Equal(got, tt.want) || (got == nil) != (tt.want == nil) {
340+ t.Errorf("prefixEnd(%x) = %x, want %x", tt.in, got, tt.want)
341+ }
342+ }
343+}
344+
345+func TestPrefixAllFF(t *testing.T) {
346+ var tr tree
347+ for i, k := range [][]byte{{0xFE}, {0xFF}, {0xFF, 0x00}, {0xFF, 0xFF, 0xFF}} {
348+ tr = tr.put(k, nil, uint64(i+1))
349+ }
350+ s := newSnapshot(tr, 4)
351+ if got := collect(s.Prefix([]byte{0xFF})); len(got) != 3 {
352+ t.Fatalf("prefix FF: %d items, want 3", len(got))
353+ }
354+ if got := collect(s.Prefix(nil)); len(got) != 4 {
355+ t.Fatalf("empty prefix: %d items, want 4", len(got))
356+ }
357+}
358+
359+func benchTree(n int) (*Snapshot, [][]byte) {
360+ keys := make([][]byte, n)
361+ b := tree{}.builder()
362+ for i := range keys {
363+ keys[i] = fmt.Appendf(nil, "key/%08d", i)
364+ b.put(keys[i], []byte("value"), uint64(i+1))
365+ }
366+ return newSnapshot(b.freeze(), uint64(n)), keys
367+}
368+
369+func BenchmarkSnapshotGet(b *testing.B) {
370+ s, keys := benchTree(100_000)
371+ b.ReportAllocs()
372+ b.ResetTimer()
373+ for i := 0; b.Loop(); i++ {
374+ if _, ok := s.Get(keys[(i*7919)%len(keys)]); !ok {
375+ b.Fatal("missing key")
376+ }
377+ }
378+}
379+
380+func BenchmarkRange(b *testing.B) {
381+ s, _ := benchTree(100_000)
382+ b.ReportAllocs()
383+ for b.Loop() {
384+ n := 0
385+ for range s.Range(nil, nil) {
386+ n++
387+ }
388+ if n != 100_000 {
389+ b.Fatalf("saw %d items", n)
390+ }
391+ }
392+}
393+
394+func BenchmarkTreePut(b *testing.B) {
395+ _, keys := benchTree(100_000)
396+ b.ReportAllocs()
397+ var tr tree
398+ for i := 0; b.Loop(); i++ {
399+ tr = tr.put(keys[(i*7919)%len(keys)], nil, uint64(i))
400+ }
401+}
402+
403+func BenchmarkBuilderPut(b *testing.B) {
404+ _, keys := benchTree(100_000)
405+ b.ReportAllocs()
406+ bl := tree{}.builder()
407+ for i := 0; b.Loop(); i++ {
408+ bl.put(keys[(i*7919)%len(keys)], nil, uint64(i))
409+ if i%64 == 63 {
410+ bl.freeze()
411+ }
412+ }
413+}
414+
415+// TestTreeDeep builds a tree of several levels, then deletes ascending,
416+// exercising merges and borrows on internal nodes.
417+func TestTreeDeep(t *testing.T) {
418+ const n = 20000
419+ var tr tree
420+ for i := range n {
421+ tr = tr.put(fmt.Appendf(nil, "%06d", i), nil, 1)
422+ }
423+ checkStructure(t, tr)
424+ depth := 0
425+ for x := tr.root; x != nil; depth++ {
426+ if x.leaf() {
427+ depth++
428+ break
429+ }
430+ x = x.children[0]
431+ }
432+ if depth < 3 {
433+ t.Fatalf("depth = %d, want >= 3", depth)
434+ }
435+ full := tr
436+ for i := range n {
437+ tr = tr.delete(fmt.Appendf(nil, "%06d", i))
438+ if i%251 == 0 {
439+ checkStructure(t, tr)
440+ }
441+ }
442+ if tr.len() != 0 || tr.root != nil {
443+ t.Fatalf("tree not empty: len=%d", tr.len())
444+ }
445+ checkStructure(t, full)
446+}
@@ -0,0 +1,446 @@
1+// SPDX-License-Identifier: MIT
2+
3+package kv
4+
5+import (
6+ "bytes"
7+ "fmt"
8+ "math/rand/v2"
9+ "slices"
10+ "testing"
11+)
12+
13+// model is the reference implementation: a map plus a sort on demand.
14+type model map[string]Item
15+
16+func (m model) clone() model {
17+ c := make(model, len(m))
18+ for k, v := range m {
19+ c[k] = v
20+ }
21+ return c
22+}
23+
24+// sorted returns the keys of m in ascending order.
25+func (m model) sorted() []string {
26+ ks := make([]string, 0, len(m))
27+ for k := range m {
28+ ks = append(ks, k)
29+ }
30+ slices.Sort(ks)
31+ return ks
32+}
33+
34+// within returns the part of the sorted keys in [start, end); nil end = unbounded.
35+func within(sorted []string, start, end []byte) []string {
36+ lo, _ := slices.BinarySearch(sorted, string(start))
37+ hi := len(sorted)
38+ if end != nil {
39+ hi, _ = slices.BinarySearch(sorted, string(end))
40+ }
41+ if hi < lo {
42+ return nil
43+ }
44+ return sorted[lo:hi]
45+}
46+
47+type kvPair struct {
48+ key string
49+ item Item
50+}
51+
52+func collect(seq func(func([]byte, Item) bool)) []kvPair {
53+ var out []kvPair
54+ seq(func(k []byte, it Item) bool {
55+ out = append(out, kvPair{string(k), it})
56+ return true
57+ })
58+ return out
59+}
60+
61+func equalItem(a, b Item) bool {
62+ return a.Version == b.Version && bytes.Equal(a.Value, b.Value)
63+}
64+
65+// checkStructure verifies the B-tree invariants: ordering, fill bounds,
66+// uniform leaf depth and the entry count.
67+func checkStructure(t *testing.T, tr tree) {
68+ t.Helper()
69+ if tr.root == nil {
70+ if tr.n != 0 {
71+ t.Fatalf("empty root but n=%d", tr.n)
72+ }
73+ return
74+ }
75+ var count, leafDepth int
76+ var prev []byte
77+ var walk func(n *node, depth int, isRoot bool)
78+ walk = func(n *node, depth int, isRoot bool) {
79+ if len(n.entries) == 0 || len(n.entries) > maxEntries || (!isRoot && len(n.entries) < minEntries) {
80+ t.Fatalf("node with %d entries at depth %d", len(n.entries), depth)
81+ }
82+ if n.leaf() {
83+ if leafDepth == 0 {
84+ leafDepth = depth
85+ } else if leafDepth != depth {
86+ t.Fatalf("leaves at depths %d and %d", leafDepth, depth)
87+ }
88+ } else if len(n.children) != len(n.entries)+1 {
89+ t.Fatalf("%d entries but %d children", len(n.entries), len(n.children))
90+ }
91+ for i, e := range n.entries {
92+ if !n.leaf() {
93+ walk(n.children[i], depth+1, false)
94+ }
95+ if prev != nil && bytes.Compare(prev, e.key) >= 0 {
96+ t.Fatalf("keys out of order: %x then %x", prev, e.key)
97+ }
98+ prev = e.key
99+ count++
100+ }
101+ if !n.leaf() {
102+ walk(n.children[len(n.entries)], depth+1, false)
103+ }
104+ }
105+ walk(tr.root, 1, true)
106+ if count != tr.n {
107+ t.Fatalf("tree has %d entries, n=%d", count, tr.n)
108+ }
109+}
110+
111+// checkAgainst compares the tree with the model through get, full scan,
112+// random ranges and random prefixes.
113+func checkAgainst(t *testing.T, rng *rand.Rand, tr tree, m model, genKey func() []byte) {
114+ t.Helper()
115+ checkStructure(t, tr)
116+ if tr.len() != len(m) {
117+ t.Fatalf("len = %d, want %d", tr.len(), len(m))
118+ }
119+ s := newSnapshot(tr, 7)
120+ if s.Len() != len(m) || s.Seq() != 7 {
121+ t.Fatalf("snapshot Len/Seq = %d/%d", s.Len(), s.Seq())
122+ }
123+ sorted := m.sorted()
124+ verify := func(what string, got []kvPair, start, end []byte) {
125+ t.Helper()
126+ want := within(sorted, start, end)
127+ if len(got) != len(want) {
128+ t.Fatalf("%s: got %d items, want %d", what, len(got), len(want))
129+ }
130+ for i, k := range want {
131+ if got[i].key != k || !equalItem(got[i].item, m[k]) {
132+ t.Fatalf("%s: item %d = %x, want %x", what, i, got[i].key, k)
133+ }
134+ }
135+ }
136+ verify("full scan", collect(s.Range(nil, nil)), nil, nil)
137+
138+ for range 20 {
139+ k := genKey()
140+ got, ok := s.Get(k)
141+ want, wok := m[string(k)]
142+ if ok != wok || (ok && !equalItem(got, want)) {
143+ t.Fatalf("Get(%x) = %v,%v want %v,%v", k, got, ok, want, wok)
144+ }
145+ }
146+ for k, want := range m {
147+ if got, ok := s.Get([]byte(k)); !ok || !equalItem(got, want) {
148+ t.Fatalf("Get(%x) = %v,%v want %v", k, got, ok, want)
149+ }
150+ break // one arbitrary present key is enough per check
151+ }
152+ for range 10 {
153+ a, b := genKey(), genKey()
154+ if bytes.Compare(a, b) > 0 {
155+ a, b = b, a
156+ }
157+ verify("range", collect(s.Range(a, b)), a, b)
158+ verify("range open end", collect(s.Range(a, nil)), a, nil)
159+ verify("range open start", collect(s.Range(nil, b)), nil, b)
160+ }
161+ for range 10 {
162+ p := genKey()
163+ p = p[:rng.IntN(len(p)+1)] // may be empty
164+ var want []string
165+ for _, k := range sorted {
166+ if bytes.HasPrefix([]byte(k), p) {
167+ want = append(want, k)
168+ }
169+ }
170+ got := collect(s.Prefix(p))
171+ if len(got) != len(want) {
172+ t.Fatalf("prefix %x: got %d items, want %d", p, len(got), len(want))
173+ }
174+ for i, k := range want {
175+ if got[i].key != k {
176+ t.Fatalf("prefix %x: item %d = %x, want %x", p, i, got[i].key, k)
177+ }
178+ }
179+ }
180+ // Early break stops the walk.
181+ seen := 0
182+ for range s.Range(nil, nil) {
183+ seen++
184+ if seen == 3 {
185+ break
186+ }
187+ }
188+ if seen != min(3, len(m)) {
189+ t.Fatalf("early break saw %d items", seen)
190+ }
191+}
192+
193+type frozen struct {
194+ tr tree
195+ m model
196+}
197+
198+func TestTreeModel(t *testing.T) {
199+ seeds, ops := 50, 10000
200+ if testing.Short() {
201+ seeds, ops = 8, 2000
202+ }
203+ alphabet := []byte{0, 1, 2, 'a', 'b', 'c', 0x7F, 0x80, 0xFD, 0xFE, 0xFF, 'x', 'y', 'z', 0x10, 0x20}
204+ for seed := range seeds {
205+ t.Run(fmt.Sprintf("seed=%d", seed), func(t *testing.T) {
206+ rng := rand.New(rand.NewPCG(uint64(seed), 0xe131))
207+ alpha := alphabet[:2+seed%(len(alphabet)-1)]
208+ genKey := func() []byte {
209+ k := make([]byte, 1+rng.IntN(5))
210+ for i := range k {
211+ k[i] = alpha[rng.IntN(len(alpha))]
212+ }
213+ return k
214+ }
215+ var (
216+ tr tree
217+ m = model{}
218+ version uint64
219+ history []frozen
220+ )
221+ apply := func(del bool, k []byte, put func(k, v []byte, ver uint64), rm func(k []byte)) {
222+ version++
223+ if del {
224+ rm(k)
225+ delete(m, string(k))
226+ return
227+ }
228+ v := make([]byte, rng.IntN(8))
229+ for i := range v {
230+ v[i] = byte(rng.IntN(256))
231+ }
232+ put(k, v, version)
233+ m[string(k)] = Item{Value: v, Version: version}
234+ }
235+ for done, next := 0, 0; done < ops; {
236+ if rng.IntN(5) == 0 {
237+ // A group of ops through one builder.
238+ b := tr.builder()
239+ n := 1 + rng.IntN(40)
240+ for range n {
241+ k := genKey()
242+ apply(rng.IntN(3) == 0, k, b.put, b.delete)
243+ if got, ok := b.get(k); ok != (m[string(k)].Version != 0) || (ok && !equalItem(got, m[string(k)])) {
244+ t.Fatalf("seed %d: builder.get mismatch", seed)
245+ }
246+ }
247+ if b.len() != len(m) {
248+ t.Fatalf("seed %d: builder.len = %d, want %d", seed, b.len(), len(m))
249+ }
250+ tr = b.freeze()
251+ done += n
252+ } else {
253+ k := genKey()
254+ apply(rng.IntN(3) == 0, k, func(k, v []byte, ver uint64) { tr = tr.put(k, v, ver) }, func(k []byte) { tr = tr.delete(k) })
255+ done++
256+ }
257+ if done >= next {
258+ next = done + 500 + rng.IntN(500)
259+ checkAgainst(t, rng, tr, m, genKey)
260+ history = append(history, frozen{tr, m.clone()})
261+ }
262+ }
263+ checkAgainst(t, rng, tr, m, genKey)
264+ // Persistence: every older tree still equals its model.
265+ for _, h := range history {
266+ checkAgainst(t, rng, h.tr, h.m, genKey)
267+ }
268+ // Drain the tree in random order, with and without a builder.
269+ keys := m.sorted()
270+ rng.Shuffle(len(keys), func(i, j int) { keys[i], keys[j] = keys[j], keys[i] })
271+ for i, k := range keys {
272+ if i%2 == 0 {
273+ tr = tr.delete([]byte(k))
274+ } else {
275+ b := tr.builder()
276+ b.delete([]byte(k))
277+ b.delete([]byte(k)) // absent: no-op
278+ tr = b.freeze()
279+ }
280+ delete(m, k)
281+ if i%97 == 0 {
282+ checkStructure(t, tr)
283+ }
284+ }
285+ checkAgainst(t, rng, tr, m, genKey)
286+ if tr.root != nil || tr.len() != 0 {
287+ t.Fatalf("seed %d: tree not empty after draining", seed)
288+ }
289+ // Draining must not have disturbed the older trees.
290+ for _, h := range history {
291+ checkAgainst(t, rng, h.tr, h.m, genKey)
292+ }
293+ })
294+ }
295+}
296+
297+// TestBuilderReuse checks that nodes of a frozen tree are never mutated by
298+// later builder operations, and that nodes owned by the builder are reused.
299+func TestBuilderReuse(t *testing.T) {
300+ var base tree
301+ for i := range 500 {
302+ base = base.put(fmt.Appendf(nil, "k%04d", i), []byte("v"), 1)
303+ }
304+ b := base.builder()
305+ b.put([]byte("k0100"), []byte("new"), 2)
306+ first := b.root
307+ b.put([]byte("k0101"), []byte("new"), 2)
308+ if b.root != first {
309+ t.Fatal("builder copied its own root twice")
310+ }
311+ frozenTree := b.freeze()
312+ b.put([]byte("k0102"), []byte("again"), 3)
313+ if b.root == frozenTree.root {
314+ t.Fatal("builder mutated the root of a frozen tree")
315+ }
316+ if it, _ := frozenTree.get([]byte("k0102")); string(it.Value) != "v" {
317+ t.Fatalf("frozen tree changed: %q", it.Value)
318+ }
319+ if it, _ := base.get([]byte("k0100")); string(it.Value) != "v" || it.Version != 1 {
320+ t.Fatalf("base tree changed: %v", it)
321+ }
322+ checkStructure(t, base)
323+ checkStructure(t, frozenTree)
324+ checkStructure(t, b.freeze())
325+}
326+
327+func TestPrefixEnd(t *testing.T) {
328+ tests := []struct{ in, want []byte }{
329+ {nil, nil},
330+ {[]byte{}, nil},
331+ {[]byte("a"), []byte("b")},
332+ {[]byte("ab"), []byte("ac")},
333+ {[]byte{'a', 0xFF}, []byte("b")},
334+ {[]byte{'a', 0xFF, 0xFF}, []byte("b")},
335+ {[]byte{0xFF}, nil},
336+ {[]byte{0xFF, 0xFF}, nil},
337+ }
338+ for _, tt := range tests {
339+ if got := prefixEnd(tt.in); !bytes.Equal(got, tt.want) || (got == nil) != (tt.want == nil) {
340+ t.Errorf("prefixEnd(%x) = %x, want %x", tt.in, got, tt.want)
341+ }
342+ }
343+}
344+
345+func TestPrefixAllFF(t *testing.T) {
346+ var tr tree
347+ for i, k := range [][]byte{{0xFE}, {0xFF}, {0xFF, 0x00}, {0xFF, 0xFF, 0xFF}} {
348+ tr = tr.put(k, nil, uint64(i+1))
349+ }
350+ s := newSnapshot(tr, 4)
351+ if got := collect(s.Prefix([]byte{0xFF})); len(got) != 3 {
352+ t.Fatalf("prefix FF: %d items, want 3", len(got))
353+ }
354+ if got := collect(s.Prefix(nil)); len(got) != 4 {
355+ t.Fatalf("empty prefix: %d items, want 4", len(got))
356+ }
357+}
358+
359+func benchTree(n int) (*Snapshot, [][]byte) {
360+ keys := make([][]byte, n)
361+ b := tree{}.builder()
362+ for i := range keys {
363+ keys[i] = fmt.Appendf(nil, "key/%08d", i)
364+ b.put(keys[i], []byte("value"), uint64(i+1))
365+ }
366+ return newSnapshot(b.freeze(), uint64(n)), keys
367+}
368+
369+func BenchmarkSnapshotGet(b *testing.B) {
370+ s, keys := benchTree(100_000)
371+ b.ReportAllocs()
372+ b.ResetTimer()
373+ for i := 0; b.Loop(); i++ {
374+ if _, ok := s.Get(keys[(i*7919)%len(keys)]); !ok {
375+ b.Fatal("missing key")
376+ }
377+ }
378+}
379+
380+func BenchmarkRange(b *testing.B) {
381+ s, _ := benchTree(100_000)
382+ b.ReportAllocs()
383+ for b.Loop() {
384+ n := 0
385+ for range s.Range(nil, nil) {
386+ n++
387+ }
388+ if n != 100_000 {
389+ b.Fatalf("saw %d items", n)
390+ }
391+ }
392+}
393+
394+func BenchmarkTreePut(b *testing.B) {
395+ _, keys := benchTree(100_000)
396+ b.ReportAllocs()
397+ var tr tree
398+ for i := 0; b.Loop(); i++ {
399+ tr = tr.put(keys[(i*7919)%len(keys)], nil, uint64(i))
400+ }
401+}
402+
403+func BenchmarkBuilderPut(b *testing.B) {
404+ _, keys := benchTree(100_000)
405+ b.ReportAllocs()
406+ bl := tree{}.builder()
407+ for i := 0; b.Loop(); i++ {
408+ bl.put(keys[(i*7919)%len(keys)], nil, uint64(i))
409+ if i%64 == 63 {
410+ bl.freeze()
411+ }
412+ }
413+}
414+
415+// TestTreeDeep builds a tree of several levels, then deletes ascending,
416+// exercising merges and borrows on internal nodes.
417+func TestTreeDeep(t *testing.T) {
418+ const n = 20000
419+ var tr tree
420+ for i := range n {
421+ tr = tr.put(fmt.Appendf(nil, "%06d", i), nil, 1)
422+ }
423+ checkStructure(t, tr)
424+ depth := 0
425+ for x := tr.root; x != nil; depth++ {
426+ if x.leaf() {
427+ depth++
428+ break
429+ }
430+ x = x.children[0]
431+ }
432+ if depth < 3 {
433+ t.Fatalf("depth = %d, want >= 3", depth)
434+ }
435+ full := tr
436+ for i := range n {
437+ tr = tr.delete(fmt.Appendf(nil, "%06d", i))
438+ if i%251 == 0 {
439+ checkStructure(t, tr)
440+ }
441+ }
442+ if tr.len() != 0 || tr.root != nil {
443+ t.Fatalf("tree not empty: len=%d", tr.len())
444+ }
445+ checkStructure(t, full)
446+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 2

↑↓ moveenter openesc close