Rename Transaction to Tx.
I changed the Transaction/RWTransaction types to Tx/RWTx, respectively. This makes the naming
more consistent with other packages such as database/sql. The txnid is changed to txid as well.
Ben Johnson
10 years ago
0 | TODO | |
1 | ==== | |
2 | X Open DB. | |
3 | X Initialize transaction. | |
4 | - Cursor First, Get(key), Next | |
5 | - RWTransaction.insert() | |
6 | - rebalance | |
7 | - adjust cursors | |
8 | - RWTransaction Commmit | |
9 | ||
10 | ||
11 | ||
12 |
6 | 6 | // Bucket represents a collection of key/value pairs inside the database. |
7 | 7 | type Bucket struct { |
8 | 8 | *bucket |
9 | name string | |
10 | transaction *Transaction | |
11 | rwtransaction *RWTransaction | |
9 | name string | |
10 | tx *Tx | |
11 | rwtx *RWTx | |
12 | 12 | } |
13 | 13 | |
14 | 14 | // bucket represents the on-file representation of a bucket. |
24 | 24 | |
25 | 25 | // Writable returns whether the bucket is writable. |
26 | 26 | func (b *Bucket) Writable() bool { |
27 | return (b.rwtransaction != nil) | |
27 | return (b.rwtx != nil) | |
28 | 28 | } |
29 | 29 | |
30 | 30 | // Cursor creates a cursor associated with the bucket. |
31 | // The cursor is only valid as long as the Transaction is open. | |
31 | // The cursor is only valid as long as the transaction is open. | |
32 | 32 | // Do not use a cursor after the transaction is closed. |
33 | 33 | func (b *Bucket) Cursor() *Cursor { |
34 | 34 | return &Cursor{ |
35 | transaction: b.transaction, | |
36 | root: b.root, | |
37 | stack: make([]elemRef, 0), | |
35 | tx: b.tx, | |
36 | root: b.root, | |
37 | stack: make([]elemRef, 0), | |
38 | 38 | } |
39 | 39 | } |
40 | 40 | |
73 | 73 | c.Seek(key) |
74 | 74 | |
75 | 75 | // Insert the key/value. |
76 | c.node(b.rwtransaction).put(key, key, value, 0) | |
76 | c.node(b.rwtx).put(key, key, value, 0) | |
77 | 77 | |
78 | 78 | return nil |
79 | 79 | } |
91 | 91 | c.Seek(key) |
92 | 92 | |
93 | 93 | // Delete the node if we have a matching key. |
94 | c.node(b.rwtransaction).del(key) | |
94 | c.node(b.rwtx).del(key) | |
95 | 95 | |
96 | 96 | return nil |
97 | 97 | } |
129 | 129 | // Stat returns stats on a bucket. |
130 | 130 | func (b *Bucket) Stat() *BucketStat { |
131 | 131 | s := &BucketStat{} |
132 | b.transaction.forEachPage(b.root, 0, func(p *page, depth int) { | |
132 | b.tx.forEachPage(b.root, 0, func(p *page, depth int) { | |
133 | 133 | if (p.flags & leafPageFlag) != 0 { |
134 | 134 | s.LeafPageCount++ |
135 | 135 | s.KeyCount += int(p.count) |
26 | 26 | func TestBucketGetFromNode(t *testing.T) { |
27 | 27 | withOpenDB(func(db *DB, path string) { |
28 | 28 | db.CreateBucket("widgets") |
29 | db.Do(func(txn *RWTransaction) error { | |
29 | db.Do(func(txn *RWTx) error { | |
30 | 30 | b := txn.Bucket("widgets") |
31 | 31 | b.Put([]byte("foo"), []byte("bar")) |
32 | 32 | value := b.Get([]byte("foo")) |
53 | 53 | func TestBucketPutReadOnly(t *testing.T) { |
54 | 54 | withOpenDB(func(db *DB, path string) { |
55 | 55 | db.CreateBucket("widgets") |
56 | db.With(func(txn *Transaction) error { | |
56 | db.With(func(txn *Tx) error { | |
57 | 57 | b := txn.Bucket("widgets") |
58 | 58 | err := b.Put([]byte("foo"), []byte("bar")) |
59 | 59 | assert.Equal(t, err, ErrBucketNotWritable) |
80 | 80 | func TestBucketDeleteReadOnly(t *testing.T) { |
81 | 81 | withOpenDB(func(db *DB, path string) { |
82 | 82 | db.CreateBucket("widgets") |
83 | db.With(func(txn *Transaction) error { | |
83 | db.With(func(txn *Tx) error { | |
84 | 84 | b := txn.Bucket("widgets") |
85 | 85 | err := b.Delete([]byte("foo")) |
86 | 86 | assert.Equal(t, err, ErrBucketNotWritable) |
119 | 119 | func TestBucketNextSequenceReadOnly(t *testing.T) { |
120 | 120 | withOpenDB(func(db *DB, path string) { |
121 | 121 | db.CreateBucket("widgets") |
122 | db.With(func(txn *Transaction) error { | |
122 | db.With(func(txn *Tx) error { | |
123 | 123 | b := txn.Bucket("widgets") |
124 | 124 | i, err := b.NextSequence() |
125 | 125 | assert.Equal(t, i, 0) |
133 | 133 | func TestBucketNextSequenceOverflow(t *testing.T) { |
134 | 134 | withOpenDB(func(db *DB, path string) { |
135 | 135 | db.CreateBucket("widgets") |
136 | db.Do(func(txn *RWTransaction) error { | |
136 | db.Do(func(txn *RWTx) error { | |
137 | 137 | b := txn.Bucket("widgets") |
138 | 138 | b.bucket.sequence = uint64(maxInt) |
139 | 139 | seq, err := b.NextSequence() |
217 | 217 | // Ensure a bucket can calculate stats. |
218 | 218 | func TestBucketStat(t *testing.T) { |
219 | 219 | withOpenDB(func(db *DB, path string) { |
220 | db.Do(func(txn *RWTransaction) error { | |
220 | db.Do(func(txn *RWTx) error { | |
221 | 221 | // Add bucket with lots of keys. |
222 | 222 | txn.CreateBucket("widgets") |
223 | 223 | b := txn.Bucket("widgets") |
240 | 240 | |
241 | 241 | return nil |
242 | 242 | }) |
243 | db.With(func(txn *Transaction) error { | |
243 | db.With(func(txn *Tx) error { | |
244 | 244 | b := txn.Bucket("widgets") |
245 | 245 | stat := b.Stat() |
246 | 246 | assert.Equal(t, stat.BranchPageCount, 15) |
316 | 316 | withOpenDB(func(db *DB, path string) { |
317 | 317 | // Bulk insert all values. |
318 | 318 | db.CreateBucket("widgets") |
319 | rwtxn, _ := db.RWTransaction() | |
319 | rwtxn, _ := db.RWTx() | |
320 | 320 | b := rwtxn.Bucket("widgets") |
321 | 321 | for _, item := range items { |
322 | 322 | assert.NoError(t, b.Put(item.Key, item.Value)) |
324 | 324 | assert.NoError(t, rwtxn.Commit()) |
325 | 325 | |
326 | 326 | // Verify all items exist. |
327 | txn, _ := db.Transaction() | |
327 | txn, _ := db.Tx() | |
328 | 328 | b = txn.Bucket("widgets") |
329 | 329 | for _, item := range items { |
330 | 330 | value := b.Get(item.Key) |
350 | 350 | withOpenDB(func(db *DB, path string) { |
351 | 351 | // Bulk insert all values. |
352 | 352 | db.CreateBucket("widgets") |
353 | rwtxn, _ := db.RWTransaction() | |
353 | rwtxn, _ := db.RWTx() | |
354 | 354 | b := rwtxn.Bucket("widgets") |
355 | 355 | for _, item := range items { |
356 | 356 | assert.NoError(t, b.Put(item.Key, item.Value)) |
362 | 362 | assert.NoError(t, db.Delete("widgets", item.Key)) |
363 | 363 | |
364 | 364 | // Anything before our deletion index should be nil. |
365 | txn, _ := db.Transaction() | |
365 | txn, _ := db.Tx() | |
366 | 366 | b := txn.Bucket("widgets") |
367 | 367 | for j, exp := range items { |
368 | 368 | if j > i { |
5 | 5 | ) |
6 | 6 | |
7 | 7 | // Cursor represents an iterator that can traverse over all key/value pairs in a bucket in sorted order. |
8 | // Cursors can be obtained from a Transaction and are valid as long as the Transaction is open. | |
8 | // Cursors can be obtained from a transaction and are valid as long as the transaction is open. | |
9 | 9 | type Cursor struct { |
10 | transaction *Transaction | |
11 | root pgid | |
12 | stack []elemRef | |
10 | tx *Tx | |
11 | root pgid | |
12 | stack []elemRef | |
13 | 13 | } |
14 | 14 | |
15 | 15 | // First moves the cursor to the first item in the bucket and returns its key and value. |
16 | 16 | // If the bucket is empty then a nil key and value are returned. |
17 | 17 | func (c *Cursor) First() (key []byte, value []byte) { |
18 | 18 | c.stack = c.stack[:0] |
19 | p, n := c.transaction.pageNode(c.root) | |
19 | p, n := c.tx.pageNode(c.root) | |
20 | 20 | c.stack = append(c.stack, elemRef{page: p, node: n, index: 0}) |
21 | 21 | c.first() |
22 | 22 | return c.keyValue() |
26 | 26 | // If the bucket is empty then a nil key and value are returned. |
27 | 27 | func (c *Cursor) Last() (key []byte, value []byte) { |
28 | 28 | c.stack = c.stack[:0] |
29 | p, n := c.transaction.pageNode(c.root) | |
29 | p, n := c.tx.pageNode(c.root) | |
30 | 30 | ref := elemRef{page: p, node: n} |
31 | 31 | ref.index = ref.count() - 1 |
32 | 32 | c.stack = append(c.stack, ref) |
115 | 115 | } else { |
116 | 116 | pgid = ref.page.branchPageElement(uint16(ref.index)).pgid |
117 | 117 | } |
118 | p, n := c.transaction.pageNode(pgid) | |
118 | p, n := c.tx.pageNode(pgid) | |
119 | 119 | c.stack = append(c.stack, elemRef{page: p, node: n, index: 0}) |
120 | 120 | } |
121 | 121 | } |
136 | 136 | } else { |
137 | 137 | pgid = ref.page.branchPageElement(uint16(ref.index)).pgid |
138 | 138 | } |
139 | p, n := c.transaction.pageNode(pgid) | |
139 | p, n := c.tx.pageNode(pgid) | |
140 | 140 | |
141 | 141 | var nextRef = elemRef{page: p, node: n} |
142 | 142 | nextRef.index = nextRef.count() - 1 |
146 | 146 | |
147 | 147 | // search recursively performs a binary search against a given page/node until it finds a given key. |
148 | 148 | func (c *Cursor) search(key []byte, pgid pgid) { |
149 | p, n := c.transaction.pageNode(pgid) | |
149 | p, n := c.tx.pageNode(pgid) | |
150 | 150 | if p != nil { |
151 | 151 | _assert((p.flags&(branchPageFlag|leafPageFlag)) != 0, "invalid page type: "+p.typ()) |
152 | 152 | } |
250 | 250 | } |
251 | 251 | |
252 | 252 | // node returns the node that the cursor is currently positioned on. |
253 | func (c *Cursor) node(t *RWTransaction) *node { | |
253 | func (c *Cursor) node(t *RWTx) *node { | |
254 | 254 | _assert(len(c.stack) > 0, "accessing a node with a zero-length cursor stack") |
255 | 255 | |
256 | 256 | // If the top of the stack is a leaf node then just return it. |
18 | 18 | // All data access is performed through transactions which can be obtained through the DB. |
19 | 19 | // All the functions on DB will return a ErrDatabaseNotOpen if accessed before Open() is called. |
20 | 20 | type DB struct { |
21 | os _os | |
22 | syscall _syscall | |
23 | path string | |
24 | file file | |
25 | metafile file | |
26 | data []byte | |
27 | meta0 *meta | |
28 | meta1 *meta | |
29 | pageSize int | |
30 | opened bool | |
31 | rwtransaction *RWTransaction | |
32 | transactions []*Transaction | |
33 | freelist *freelist | |
21 | os _os | |
22 | syscall _syscall | |
23 | path string | |
24 | file file | |
25 | metafile file | |
26 | data []byte | |
27 | meta0 *meta | |
28 | meta1 *meta | |
29 | pageSize int | |
30 | opened bool | |
31 | rwtx *RWTx | |
32 | txs []*Tx | |
33 | freelist *freelist | |
34 | 34 | |
35 | 35 | rwlock sync.Mutex // Allows only one writer at a time. |
36 | 36 | metalock sync.Mutex // Protects meta page access. |
111 | 111 | } |
112 | 112 | |
113 | 113 | // Read in the freelist. |
114 | db.freelist = &freelist{pending: make(map[txnid][]pgid)} | |
114 | db.freelist = &freelist{pending: make(map[txid][]pgid)} | |
115 | 115 | db.freelist.read(db.page(db.meta().freelist)) |
116 | 116 | |
117 | 117 | // Mark the database as opened and return. |
126 | 126 | defer db.mmaplock.Unlock() |
127 | 127 | |
128 | 128 | // Dereference all mmap references before unmapping. |
129 | if db.rwtransaction != nil { | |
130 | db.rwtransaction.dereference() | |
129 | if db.rwtx != nil { | |
130 | db.rwtx.dereference() | |
131 | 131 | } |
132 | 132 | |
133 | 133 | // Unmap existing data before continuing. |
217 | 217 | m.freelist = 2 |
218 | 218 | m.buckets = 3 |
219 | 219 | m.pgid = 4 |
220 | m.txnid = txnid(i) | |
220 | m.txid = txid(i) | |
221 | 221 | } |
222 | 222 | |
223 | 223 | // Write an empty freelist at page 3. |
258 | 258 | db.munmap() |
259 | 259 | } |
260 | 260 | |
261 | // Transaction creates a read-only transaction. | |
261 | // Tx creates a read-only transaction. | |
262 | 262 | // Multiple read-only transactions can be used concurrently. |
263 | 263 | // |
264 | 264 | // IMPORTANT: You must close the transaction after you are finished or else the database will not reclaim old pages. |
265 | func (db *DB) Transaction() (*Transaction, error) { | |
265 | func (db *DB) Tx() (*Tx, error) { | |
266 | 266 | db.metalock.Lock() |
267 | 267 | defer db.metalock.Unlock() |
268 | 268 | |
277 | 277 | } |
278 | 278 | |
279 | 279 | // Create a transaction associated with the database. |
280 | t := &Transaction{} | |
280 | t := &Tx{} | |
281 | 281 | t.init(db) |
282 | 282 | |
283 | 283 | // Keep track of transaction until it closes. |
284 | db.transactions = append(db.transactions, t) | |
284 | db.txs = append(db.txs, t) | |
285 | 285 | |
286 | 286 | return t, nil |
287 | 287 | } |
288 | 288 | |
289 | // RWTransaction creates a read/write transaction. | |
289 | // RWTx creates a read/write transaction. | |
290 | 290 | // Only one read/write transaction is allowed at a time. |
291 | 291 | // You must call Commit() or Rollback() on the transaction to close it. |
292 | func (db *DB) RWTransaction() (*RWTransaction, error) { | |
292 | func (db *DB) RWTx() (*RWTx, error) { | |
293 | 293 | db.metalock.Lock() |
294 | 294 | defer db.metalock.Unlock() |
295 | 295 | |
296 | // Obtain writer lock. This is released by the RWTransaction when it closes. | |
296 | // Obtain writer lock. This is released by the RWTx when it closes. | |
297 | 297 | db.rwlock.Lock() |
298 | 298 | |
299 | 299 | // Exit if the database is not open yet. |
303 | 303 | } |
304 | 304 | |
305 | 305 | // Create a transaction associated with the database. |
306 | t := &RWTransaction{} | |
306 | t := &RWTx{} | |
307 | 307 | t.init(db) |
308 | db.rwtransaction = t | |
308 | db.rwtx = t | |
309 | 309 | |
310 | 310 | // Free any pages associated with closed read-only transactions. |
311 | var minid txnid = 0xFFFFFFFFFFFFFFFF | |
312 | for _, t := range db.transactions { | |
311 | var minid txid = 0xFFFFFFFFFFFFFFFF | |
312 | for _, t := range db.txs { | |
313 | 313 | if t.id() < minid { |
314 | 314 | minid = t.id() |
315 | 315 | } |
321 | 321 | return t, nil |
322 | 322 | } |
323 | 323 | |
324 | // removeTransaction removes a transaction from the database. | |
325 | func (db *DB) removeTransaction(t *Transaction) { | |
324 | // removeTx removes a transaction from the database. | |
325 | func (db *DB) removeTx(t *Tx) { | |
326 | 326 | db.metalock.Lock() |
327 | 327 | defer db.metalock.Unlock() |
328 | 328 | |
330 | 330 | db.mmaplock.RUnlock() |
331 | 331 | |
332 | 332 | // Remove the transaction. |
333 | for i, txn := range db.transactions { | |
333 | for i, txn := range db.txs { | |
334 | 334 | if txn == t { |
335 | db.transactions = append(db.transactions[:i], db.transactions[i+1:]...) | |
335 | db.txs = append(db.txs[:i], db.txs[i+1:]...) | |
336 | 336 | break |
337 | 337 | } |
338 | 338 | } |
339 | 339 | } |
340 | 340 | |
341 | // Do executes a function within the context of a RWTransaction. | |
341 | // Do executes a function within the context of a RWTx. | |
342 | 342 | // If no error is returned from the function then the transaction is committed. |
343 | 343 | // If an error is returned then the entire transaction is rolled back. |
344 | 344 | // Any error that is returned from the function or returned from the commit is |
345 | 345 | // returned from the Do() method. |
346 | func (db *DB) Do(fn func(*RWTransaction) error) error { | |
347 | t, err := db.RWTransaction() | |
346 | func (db *DB) Do(fn func(*RWTx) error) error { | |
347 | t, err := db.RWTx() | |
348 | 348 | if err != nil { |
349 | 349 | return err |
350 | 350 | } |
358 | 358 | return t.Commit() |
359 | 359 | } |
360 | 360 | |
361 | // With executes a function within the context of a Transaction. | |
361 | // With executes a function within the context of a transaction. | |
362 | 362 | // Any error that is returned from the function is returned from the With() method. |
363 | func (db *DB) With(fn func(*Transaction) error) error { | |
364 | t, err := db.Transaction() | |
363 | func (db *DB) With(fn func(*Tx) error) error { | |
364 | t, err := db.Tx() | |
365 | 365 | if err != nil { |
366 | 366 | return err |
367 | 367 | } |
374 | 374 | // ForEach executes a function for each key/value pair in a bucket. |
375 | 375 | // An error is returned if the bucket cannot be found. |
376 | 376 | func (db *DB) ForEach(name string, fn func(k, v []byte) error) error { |
377 | return db.With(func(t *Transaction) error { | |
377 | return db.With(func(t *Tx) error { | |
378 | 378 | b := t.Bucket(name) |
379 | 379 | if b == nil { |
380 | 380 | return ErrBucketNotFound |
386 | 386 | // Bucket retrieves a reference to a bucket. |
387 | 387 | // This is typically useful for checking the existence of a bucket. |
388 | 388 | func (db *DB) Bucket(name string) (*Bucket, error) { |
389 | t, err := db.Transaction() | |
389 | t, err := db.Tx() | |
390 | 390 | if err != nil { |
391 | 391 | return nil, err |
392 | 392 | } |
396 | 396 | |
397 | 397 | // Buckets retrieves a list of all buckets in the database. |
398 | 398 | func (db *DB) Buckets() ([]*Bucket, error) { |
399 | t, err := db.Transaction() | |
399 | t, err := db.Tx() | |
400 | 400 | if err != nil { |
401 | 401 | return nil, err |
402 | 402 | } |
408 | 408 | // This function can return an error if the bucket already exists, if the name |
409 | 409 | // is blank, or the bucket name is too long. |
410 | 410 | func (db *DB) CreateBucket(name string) error { |
411 | return db.Do(func(t *RWTransaction) error { | |
411 | return db.Do(func(t *RWTx) error { | |
412 | 412 | return t.CreateBucket(name) |
413 | 413 | }) |
414 | 414 | } |
416 | 416 | // CreateBucketIfNotExists creates a new bucket with the given name if it doesn't already exist. |
417 | 417 | // This function can return an error if the name is blank, or the bucket name is too long. |
418 | 418 | func (db *DB) CreateBucketIfNotExists(name string) error { |
419 | return db.Do(func(t *RWTransaction) error { | |
419 | return db.Do(func(t *RWTx) error { | |
420 | 420 | return t.CreateBucketIfNotExists(name) |
421 | 421 | }) |
422 | 422 | } |
424 | 424 | // DeleteBucket removes a bucket from the database. |
425 | 425 | // Returns an error if the bucket does not exist. |
426 | 426 | func (db *DB) DeleteBucket(name string) error { |
427 | return db.Do(func(t *RWTransaction) error { | |
427 | return db.Do(func(t *RWTx) error { | |
428 | 428 | return t.DeleteBucket(name) |
429 | 429 | }) |
430 | 430 | } |
433 | 433 | // This function can return an error if the bucket does not exist. |
434 | 434 | func (db *DB) NextSequence(name string) (int, error) { |
435 | 435 | var seq int |
436 | err := db.Do(func(t *RWTransaction) error { | |
436 | err := db.Do(func(t *RWTx) error { | |
437 | 437 | b := t.Bucket(name) |
438 | 438 | if b == nil { |
439 | 439 | return ErrBucketNotFound |
452 | 452 | // Get retrieves the value for a key in a bucket. |
453 | 453 | // Returns an error if the key does not exist. |
454 | 454 | func (db *DB) Get(name string, key []byte) ([]byte, error) { |
455 | t, err := db.Transaction() | |
455 | t, err := db.Tx() | |
456 | 456 | if err != nil { |
457 | 457 | return nil, err |
458 | 458 | } |
481 | 481 | // Put sets the value for a key in a bucket. |
482 | 482 | // Returns an error if the bucket is not found, if key is blank, if the key is too large, or if the value is too large. |
483 | 483 | func (db *DB) Put(name string, key []byte, value []byte) error { |
484 | return db.Do(func(t *RWTransaction) error { | |
484 | return db.Do(func(t *RWTx) error { | |
485 | 485 | b := t.Bucket(name) |
486 | 486 | if b == nil { |
487 | 487 | return ErrBucketNotFound |
493 | 493 | // Delete removes a key from a bucket. |
494 | 494 | // Returns an error if the bucket cannot be found. |
495 | 495 | func (db *DB) Delete(name string, key []byte) error { |
496 | return db.Do(func(t *RWTransaction) error { | |
496 | return db.Do(func(t *RWTx) error { | |
497 | 497 | b := t.Bucket(name) |
498 | 498 | if b == nil { |
499 | 499 | return ErrBucketNotFound |
507 | 507 | // using the database while a copy is in progress. |
508 | 508 | func (db *DB) Copy(w io.Writer) error { |
509 | 509 | // Maintain a reader transaction so pages don't get reclaimed. |
510 | t, err := db.Transaction() | |
510 | t, err := db.Tx() | |
511 | 511 | if err != nil { |
512 | 512 | return err |
513 | 513 | } |
548 | 548 | db.mmaplock.RLock() |
549 | 549 | |
550 | 550 | var s = &Stat{ |
551 | MmapSize: len(db.data), | |
552 | TransactionCount: len(db.transactions), | |
551 | MmapSize: len(db.data), | |
552 | TxCount: len(db.txs), | |
553 | 553 | } |
554 | 554 | |
555 | 555 | // Release locks. |
556 | 556 | db.mmaplock.RUnlock() |
557 | 557 | db.metalock.Unlock() |
558 | 558 | |
559 | err := db.Do(func(t *RWTransaction) error { | |
559 | err := db.Do(func(t *RWTx) error { | |
560 | 560 | s.PageCount = int(t.meta.pgid) |
561 | 561 | s.FreePageCount = len(db.freelist.all()) |
562 | 562 | s.PageSize = db.pageSize |
581 | 581 | |
582 | 582 | // meta retrieves the current meta page reference. |
583 | 583 | func (db *DB) meta() *meta { |
584 | if db.meta0.txnid > db.meta1.txnid { | |
584 | if db.meta0.txid > db.meta1.txid { | |
585 | 585 | return db.meta0 |
586 | 586 | } |
587 | 587 | return db.meta1 |
600 | 600 | } |
601 | 601 | |
602 | 602 | // Resize mmap() if we're at the end. |
603 | p.id = db.rwtransaction.meta.pgid | |
603 | p.id = db.rwtx.meta.pgid | |
604 | 604 | var minsz = int((p.id+pgid(count))+1) * db.pageSize |
605 | 605 | if minsz >= len(db.data) { |
606 | 606 | if err := db.mmap(minsz); err != nil { |
609 | 609 | } |
610 | 610 | |
611 | 611 | // Move the page id high water mark. |
612 | db.rwtransaction.meta.pgid += pgid(count) | |
612 | db.rwtx.meta.pgid += pgid(count) | |
613 | 613 | |
614 | 614 | return p, nil |
615 | 615 | } |
633 | 633 | // resize it. |
634 | 634 | MmapSize int |
635 | 635 | |
636 | // TransactionCount is the total number of reader transactions. | |
637 | TransactionCount int | |
638 | } | |
636 | // TxCount is the total number of reader transactions. | |
637 | TxCount int | |
638 | } |
155 | 155 | } |
156 | 156 | |
157 | 157 | // Ensure that a database cannot open a transaction when it's not open. |
158 | func TestDBTransactionErrDatabaseNotOpen(t *testing.T) { | |
159 | withDB(func(db *DB, path string) { | |
160 | txn, err := db.Transaction() | |
158 | func TestDBTxErrDatabaseNotOpen(t *testing.T) { | |
159 | withDB(func(db *DB, path string) { | |
160 | txn, err := db.Tx() | |
161 | 161 | assert.Nil(t, txn) |
162 | 162 | assert.Equal(t, err, ErrDatabaseNotOpen) |
163 | 163 | }) |
172 | 172 | } |
173 | 173 | |
174 | 174 | // Ensure a database can provide a transactional block. |
175 | func TestDBTransactionBlock(t *testing.T) { | |
176 | withOpenDB(func(db *DB, path string) { | |
177 | err := db.Do(func(txn *RWTransaction) error { | |
175 | func TestDBTxBlock(t *testing.T) { | |
176 | withOpenDB(func(db *DB, path string) { | |
177 | err := db.Do(func(txn *RWTx) error { | |
178 | 178 | txn.CreateBucket("widgets") |
179 | 179 | b := txn.Bucket("widgets") |
180 | 180 | b.Put([]byte("foo"), []byte("bar")) |
191 | 191 | } |
192 | 192 | |
193 | 193 | // Ensure a closed database returns an error while running a transaction block |
194 | func TestDBTransactionBlockWhileClosed(t *testing.T) { | |
195 | withDB(func(db *DB, path string) { | |
196 | err := db.Do(func(txn *RWTransaction) error { | |
194 | func TestDBTxBlockWhileClosed(t *testing.T) { | |
195 | withDB(func(db *DB, path string) { | |
196 | err := db.Do(func(txn *RWTx) error { | |
197 | 197 | txn.CreateBucket("widgets") |
198 | 198 | return nil |
199 | 199 | }) |
275 | 275 | // Ensure the database can return stats about itself. |
276 | 276 | func TestDBStat(t *testing.T) { |
277 | 277 | withOpenDB(func(db *DB, path string) { |
278 | db.Do(func(txn *RWTransaction) error { | |
278 | db.Do(func(txn *RWTx) error { | |
279 | 279 | txn.CreateBucket("widgets") |
280 | 280 | b := txn.Bucket("widgets") |
281 | 281 | for i := 0; i < 10000; i++ { |
289 | 289 | db.Delete("widgets", []byte("1000")) |
290 | 290 | |
291 | 291 | // Open some readers. |
292 | t0, _ := db.Transaction() | |
293 | t1, _ := db.Transaction() | |
294 | t2, _ := db.Transaction() | |
292 | t0, _ := db.Tx() | |
293 | t1, _ := db.Tx() | |
294 | t2, _ := db.Tx() | |
295 | 295 | t2.Close() |
296 | 296 | |
297 | 297 | // Obtain stats. |
301 | 301 | assert.Equal(t, stat.FreePageCount, 2) |
302 | 302 | assert.Equal(t, stat.PageSize, 4096) |
303 | 303 | assert.Equal(t, stat.MmapSize, 4194304) |
304 | assert.Equal(t, stat.TransactionCount, 2) | |
304 | assert.Equal(t, stat.TxCount, 2) | |
305 | 305 | |
306 | 306 | // Close readers. |
307 | 307 | t0.Close() |
1 | 1 | Package bolt implements a low-level key/value store in pure Go. It supports |
2 | 2 | fully serializable transactions, ACID semantics, and lock-free MVCC with |
3 | 3 | multiple readers and a single writer. Bolt can be used for projects that |
4 | want a simple data store without the need to add large dependencies such as | |
4 | want a simple data store without the need to add large dependencies such as | |
5 | 5 | Postgres or MySQL. |
6 | 6 | |
7 | 7 | Bolt is a single-level, zero-copy, B+tree data store. This means that Bolt is |
13 | 13 | |
14 | 14 | Basics |
15 | 15 | |
16 | There are only a few types in Bolt: DB, Bucket, Transaction, RWTransaction, and | |
17 | Cursor. The DB is a collection of buckets and is represented by a single file | |
18 | on disk. A bucket is a collection of unique keys that are associated with values. | |
16 | There are only a few types in Bolt: DB, Bucket, Tx, RWTx, and Cursor. The DB is | |
17 | a collection of buckets and is represented by a single file on disk. A bucket is | |
18 | a collection of unique keys that are associated with values. | |
19 | 19 | |
20 | Transactions provide read-only access to data inside the database. They can | |
21 | retrieve key/value pairs and can use Cursors to iterate over the entire dataset. | |
22 | RWTransactions provide read-write access to the database. They can create and | |
23 | delete buckets and they can insert and remove keys. Only one RWTransaction is | |
24 | allowed at a time. | |
20 | Txs provide read-only access to data inside the database. They can retrieve | |
21 | key/value pairs and can use Cursors to iterate over the entire dataset. RWTxs | |
22 | provide read-write access to the database. They can create and delete buckets | |
23 | and they can insert and remove keys. Only one RWTx is allowed at a time. | |
25 | 24 | |
26 | 25 | |
27 | 26 | Caveats |
66 | 66 | defer db.Close() |
67 | 67 | |
68 | 68 | // Execute several commands within a write transaction. |
69 | err := db.Do(func(t *RWTransaction) error { | |
69 | err := db.Do(func(t *RWTx) error { | |
70 | 70 | if err := t.CreateBucket("widgets"); err != nil { |
71 | 71 | return err |
72 | 72 | } |
99 | 99 | db.Put("people", []byte("susy"), []byte("que")) |
100 | 100 | |
101 | 101 | // Access data from within a read-only transactional block. |
102 | db.With(func(t *Transaction) error { | |
102 | db.With(func(t *Tx) error { | |
103 | 103 | v := t.Bucket("people").Get([]byte("john")) |
104 | 104 | fmt.Printf("John's last name is %s.\n", string(v)) |
105 | 105 | return nil |
133 | 133 | // A liger is awesome. |
134 | 134 | } |
135 | 135 | |
136 | func ExampleRWTransaction() { | |
137 | // Open the database. | |
138 | var db DB | |
139 | db.Open("/tmp/bolt/rwtransaction.db", 0666) | |
136 | func ExampleRWTx() { | |
137 | // Open the database. | |
138 | var db DB | |
139 | db.Open("/tmp/bolt/rwtx.db", 0666) | |
140 | 140 | defer db.Close() |
141 | 141 | |
142 | 142 | // Create a bucket. |
143 | 143 | db.CreateBucket("widgets") |
144 | 144 | |
145 | 145 | // Create several keys in a transaction. |
146 | rwtxn, _ := db.RWTransaction() | |
146 | rwtxn, _ := db.RWTx() | |
147 | 147 | b := rwtxn.Bucket("widgets") |
148 | 148 | b.Put([]byte("john"), []byte("blue")) |
149 | 149 | b.Put([]byte("abby"), []byte("red")) |
151 | 151 | rwtxn.Commit() |
152 | 152 | |
153 | 153 | // Iterate over the values in sorted key order. |
154 | txn, _ := db.Transaction() | |
154 | txn, _ := db.Tx() | |
155 | 155 | c := txn.Bucket("widgets").Cursor() |
156 | 156 | for k, v := c.First(); k != nil; k, v = c.Next() { |
157 | 157 | fmt.Printf("%s likes %s\n", string(k), string(v)) |
164 | 164 | // zephyr likes purple |
165 | 165 | } |
166 | 166 | |
167 | func ExampleRWTransaction_rollback() { | |
168 | // Open the database. | |
169 | var db DB | |
170 | db.Open("/tmp/bolt/rwtransaction_rollback.db", 0666) | |
167 | func ExampleRWTx_rollback() { | |
168 | // Open the database. | |
169 | var db DB | |
170 | db.Open("/tmp/bolt/rwtx_rollback.db", 0666) | |
171 | 171 | defer db.Close() |
172 | 172 | |
173 | 173 | // Create a bucket. |
177 | 177 | db.Put("widgets", []byte("foo"), []byte("bar")) |
178 | 178 | |
179 | 179 | // Update the key but rollback the transaction so it never saves. |
180 | rwtxn, _ := db.RWTransaction() | |
180 | rwtxn, _ := db.RWTx() | |
181 | 181 | b := rwtxn.Bucket("widgets") |
182 | 182 | b.Put([]byte("foo"), []byte("baz")) |
183 | 183 | rwtxn.Rollback() |
8 | 8 | // It also tracks pages that have been freed but are still in use by open transactions. |
9 | 9 | type freelist struct { |
10 | 10 | ids []pgid |
11 | pending map[txnid][]pgid | |
11 | pending map[txid][]pgid | |
12 | 12 | } |
13 | 13 | |
14 | 14 | // all returns a list of all free ids and all pending ids in one sorted list. |
49 | 49 | } |
50 | 50 | |
51 | 51 | // free releases a page and its overflow for a given transaction id. |
52 | func (f *freelist) free(txnid txnid, p *page) { | |
53 | var ids = f.pending[txnid] | |
52 | func (f *freelist) free(txid txid, p *page) { | |
53 | var ids = f.pending[txid] | |
54 | 54 | _assert(p.id > 1, "cannot free page 0 or 1: %d", p.id) |
55 | 55 | for i := 0; i < int(p.overflow+1); i++ { |
56 | 56 | ids = append(ids, p.id+pgid(i)) |
57 | 57 | } |
58 | f.pending[txnid] = ids | |
58 | f.pending[txid] = ids | |
59 | 59 | } |
60 | 60 | |
61 | 61 | // release moves all page ids for a transaction id (or older) to the freelist. |
62 | func (f *freelist) release(txnid txnid) { | |
62 | func (f *freelist) release(txid txid) { | |
63 | 63 | for tid, ids := range f.pending { |
64 | if tid <= txnid { | |
64 | if tid <= txid { | |
65 | 65 | f.ids = append(f.ids, ids...) |
66 | 66 | delete(f.pending, tid) |
67 | 67 | } |
8 | 8 | |
9 | 9 | // Ensure that a page is added to a transaction's freelist. |
10 | 10 | func TestFreelistFree(t *testing.T) { |
11 | f := &freelist{pending: make(map[txnid][]pgid)} | |
11 | f := &freelist{pending: make(map[txid][]pgid)} | |
12 | 12 | f.free(100, &page{id: 12}) |
13 | 13 | assert.Equal(t, f.pending[100], []pgid{12}) |
14 | 14 | } |
15 | 15 | |
16 | 16 | // Ensure that a page and its overflow is added to a transaction's freelist. |
17 | 17 | func TestFreelistFreeOverflow(t *testing.T) { |
18 | f := &freelist{pending: make(map[txnid][]pgid)} | |
18 | f := &freelist{pending: make(map[txid][]pgid)} | |
19 | 19 | f.free(100, &page{id: 12, overflow: 3}) |
20 | 20 | assert.Equal(t, f.pending[100], []pgid{12, 13, 14, 15}) |
21 | 21 | } |
22 | 22 | |
23 | 23 | // Ensure that a transaction's free pages can be released. |
24 | 24 | func TestFreelistRelease(t *testing.T) { |
25 | f := &freelist{pending: make(map[txnid][]pgid)} | |
25 | f := &freelist{pending: make(map[txid][]pgid)} | |
26 | 26 | f.free(100, &page{id: 12, overflow: 1}) |
27 | 27 | f.free(100, &page{id: 9}) |
28 | 28 | f.free(102, &page{id: 39}) |
60 | 60 | ids[1] = 50 |
61 | 61 | |
62 | 62 | // Deserialize page into a freelist. |
63 | f := &freelist{pending: make(map[txnid][]pgid)} | |
63 | f := &freelist{pending: make(map[txid][]pgid)} | |
64 | 64 | f.read(page) |
65 | 65 | |
66 | 66 | // Ensure that there are two page ids in the freelist. |
73 | 73 | func TestFreelistWrite(t *testing.T) { |
74 | 74 | // Create a freelist and write it to a page. |
75 | 75 | var buf [4096]byte |
76 | f := &freelist{ids: []pgid{12, 39}, pending: make(map[txnid][]pgid)} | |
76 | f := &freelist{ids: []pgid{12, 39}, pending: make(map[txid][]pgid)} | |
77 | 77 | f.pending[100] = []pgid{28, 11} |
78 | 78 | f.pending[101] = []pgid{3} |
79 | 79 | p := (*page)(unsafe.Pointer(&buf[0])) |
80 | 80 | f.write(p) |
81 | 81 | |
82 | 82 | // Read the page back out. |
83 | f2 := &freelist{pending: make(map[txnid][]pgid)} | |
83 | f2 := &freelist{pending: make(map[txid][]pgid)} | |
84 | 84 | f2.read(p) |
85 | 85 | |
86 | 86 | // Ensure that the freelist is correct. |
11 | 11 | ) |
12 | 12 | |
13 | 13 | // Ensure that multiple threads can use the DB without race detector errors. |
14 | func TestParallelTransactions(t *testing.T) { | |
14 | func TestParallelTxs(t *testing.T) { | |
15 | 15 | var mutex sync.RWMutex |
16 | 16 | |
17 | 17 | err := quick.Check(func(numReaders, batchSize uint, items testdata) bool { |
44 | 44 | go func() { |
45 | 45 | mutex.RLock() |
46 | 46 | local := current |
47 | txn, err := db.Transaction() | |
47 | txn, err := db.Tx() | |
48 | 48 | mutex.RUnlock() |
49 | 49 | if err == ErrDatabaseNotOpen { |
50 | 50 | wg.Done() |
82 | 82 | pending = pending[currentBatchSize:] |
83 | 83 | |
84 | 84 | // Start write transaction. |
85 | txn, err := db.RWTransaction() | |
85 | txn, err := db.RWTx() | |
86 | 86 | if !assert.NoError(t, err) { |
87 | 87 | t.FailNow() |
88 | 88 | } |
9 | 9 | buckets pgid |
10 | 10 | freelist pgid |
11 | 11 | pgid pgid |
12 | txnid txnid | |
12 | txid txid | |
13 | 13 | } |
14 | 14 | |
15 | 15 | // validate checks the marker bytes and version of the meta page to ensure it matches this binary. |
30 | 30 | dest.buckets = m.buckets |
31 | 31 | dest.freelist = m.freelist |
32 | 32 | dest.pgid = m.pgid |
33 | dest.txnid = m.txnid | |
33 | dest.txid = m.txid | |
34 | 34 | } |
35 | 35 | |
36 | 36 | // write writes the meta onto a page. |
37 | 37 | func (m *meta) write(p *page) { |
38 | 38 | // Page id is either going to be 0 or 1 which we can determine by the Txn ID. |
39 | p.id = pgid(m.txnid % 2) | |
39 | p.id = pgid(m.txid % 2) | |
40 | 40 | p.flags |= metaPageFlag |
41 | 41 | |
42 | 42 | m.copy(p.meta()) |
7 | 7 | |
8 | 8 | // node represents an in-memory, deserialized page. |
9 | 9 | type node struct { |
10 | transaction *RWTransaction | |
11 | isLeaf bool | |
12 | unbalanced bool | |
13 | key []byte | |
14 | depth int | |
15 | pgid pgid | |
16 | parent *node | |
17 | inodes inodes | |
10 | tx *RWTx | |
11 | isLeaf bool | |
12 | unbalanced bool | |
13 | key []byte | |
14 | depth int | |
15 | pgid pgid | |
16 | parent *node | |
17 | inodes inodes | |
18 | 18 | } |
19 | 19 | |
20 | 20 | // minKeys returns the minimum number of inodes this node should have. |
55 | 55 | // childAt returns the child node at a given index. |
56 | 56 | func (n *node) childAt(index int) *node { |
57 | 57 | _assert(!n.isLeaf, "invalid childAt(%d) on a leaf node", index) |
58 | return n.transaction.node(n.inodes[index].pgid, n) | |
58 | return n.tx.node(n.inodes[index].pgid, n) | |
59 | 59 | } |
60 | 60 | |
61 | 61 | // childIndex returns the index of a given child node. |
213 | 213 | if len(current.inodes) >= minKeysPerPage && i < len(inodes)-minKeysPerPage && size+elemSize > threshold { |
214 | 214 | size = pageHeaderSize |
215 | 215 | nodes = append(nodes, current) |
216 | current = &node{transaction: n.transaction, isLeaf: n.isLeaf} | |
216 | current = &node{tx: n.tx, isLeaf: n.isLeaf} | |
217 | 217 | } |
218 | 218 | |
219 | 219 | size += elemSize |
233 | 233 | n.unbalanced = false |
234 | 234 | |
235 | 235 | // Ignore if node is above threshold (25%) and has enough keys. |
236 | var threshold = n.transaction.db.pageSize / 4 | |
236 | var threshold = n.tx.db.pageSize / 4 | |
237 | 237 | if n.size() > threshold && len(n.inodes) > n.minKeys() { |
238 | 238 | return |
239 | 239 | } |
243 | 243 | // If root node is a branch and only has one node then collapse it. |
244 | 244 | if !n.isLeaf && len(n.inodes) == 1 { |
245 | 245 | // Move child's children up. |
246 | child := n.transaction.nodes[n.inodes[0].pgid] | |
246 | child := n.tx.nodes[n.inodes[0].pgid] | |
247 | 247 | n.isLeaf = child.isLeaf |
248 | 248 | n.inodes = child.inodes[:] |
249 | 249 | |
250 | 250 | // Reparent all child nodes being moved. |
251 | 251 | for _, inode := range n.inodes { |
252 | if child, ok := n.transaction.nodes[inode.pgid]; ok { | |
252 | if child, ok := n.tx.nodes[inode.pgid]; ok { | |
253 | 253 | child.parent = n |
254 | 254 | } |
255 | 255 | } |
256 | 256 | |
257 | 257 | // Remove old child. |
258 | 258 | child.parent = nil |
259 | delete(n.transaction.nodes, child.pgid) | |
259 | delete(n.tx.nodes, child.pgid) | |
260 | 260 | } |
261 | 261 | |
262 | 262 | return |
277 | 277 | if target.numChildren() > target.minKeys() { |
278 | 278 | if useNextSibling { |
279 | 279 | // Reparent and move node. |
280 | if child, ok := n.transaction.nodes[target.inodes[0].pgid]; ok { | |
280 | if child, ok := n.tx.nodes[target.inodes[0].pgid]; ok { | |
281 | 281 | child.parent = n |
282 | 282 | } |
283 | 283 | n.inodes = append(n.inodes, target.inodes[0]) |
288 | 288 | target.key = target.inodes[0].key |
289 | 289 | } else { |
290 | 290 | // Reparent and move node. |
291 | if child, ok := n.transaction.nodes[target.inodes[len(target.inodes)-1].pgid]; ok { | |
291 | if child, ok := n.tx.nodes[target.inodes[len(target.inodes)-1].pgid]; ok { | |
292 | 292 | child.parent = n |
293 | 293 | } |
294 | 294 | n.inodes = append(n.inodes, inode{}) |
308 | 308 | if useNextSibling { |
309 | 309 | // Reparent all child nodes being moved. |
310 | 310 | for _, inode := range target.inodes { |
311 | if child, ok := n.transaction.nodes[inode.pgid]; ok { | |
311 | if child, ok := n.tx.nodes[inode.pgid]; ok { | |
312 | 312 | child.parent = n |
313 | 313 | } |
314 | 314 | } |
316 | 316 | // Copy over inodes from target and remove target. |
317 | 317 | n.inodes = append(n.inodes, target.inodes...) |
318 | 318 | n.parent.del(target.key) |
319 | delete(n.transaction.nodes, target.pgid) | |
319 | delete(n.tx.nodes, target.pgid) | |
320 | 320 | } else { |
321 | 321 | // Reparent all child nodes being moved. |
322 | 322 | for _, inode := range n.inodes { |
323 | if child, ok := n.transaction.nodes[inode.pgid]; ok { | |
323 | if child, ok := n.tx.nodes[inode.pgid]; ok { | |
324 | 324 | child.parent = target |
325 | 325 | } |
326 | 326 | } |
329 | 329 | target.inodes = append(target.inodes, n.inodes...) |
330 | 330 | n.parent.del(n.key) |
331 | 331 | n.parent.put(target.key, target.inodes[0].key, nil, target.pgid) |
332 | delete(n.transaction.nodes, n.pgid) | |
332 | delete(n.tx.nodes, n.pgid) | |
333 | 333 | } |
334 | 334 | |
335 | 335 | // Either this node or the target node was deleted from the parent so rebalance it. |
4 | 4 | "unsafe" |
5 | 5 | ) |
6 | 6 | |
7 | // RWTransaction represents a transaction that can read and write data. | |
7 | // RWTx represents a transaction that can read and write data. | |
8 | 8 | // Only one read/write transaction can be active for a database at a time. |
9 | // RWTransaction is composed of a read-only Transaction so it can also use | |
10 | // functions provided by Transaction. | |
11 | type RWTransaction struct { | |
12 | Transaction | |
9 | // RWTx is composed of a read-only transaction so it can also use | |
10 | // functions provided by Tx. | |
11 | type RWTx struct { | |
12 | Tx | |
13 | 13 | pending []*node |
14 | 14 | } |
15 | 15 | |
16 | 16 | // init initializes the transaction. |
17 | func (t *RWTransaction) init(db *DB) { | |
18 | t.Transaction.init(db) | |
19 | t.Transaction.rwtransaction = t | |
17 | func (t *RWTx) init(db *DB) { | |
18 | t.Tx.init(db) | |
19 | t.Tx.rwtx = t | |
20 | 20 | t.pages = make(map[pgid]*page) |
21 | 21 | t.nodes = make(map[pgid]*node) |
22 | 22 | |
23 | 23 | // Increment the transaction id. |
24 | t.meta.txnid += txnid(1) | |
24 | t.meta.txid += txid(1) | |
25 | 25 | } |
26 | 26 | |
27 | 27 | // CreateBucket creates a new bucket. |
28 | 28 | // Returns an error if the bucket already exists, if the bucket name is blank, or if the bucket name is too long. |
29 | func (t *RWTransaction) CreateBucket(name string) error { | |
29 | func (t *RWTx) CreateBucket(name string) error { | |
30 | 30 | // Check if bucket already exists. |
31 | 31 | if b := t.Bucket(name); b != nil { |
32 | 32 | return ErrBucketExists |
51 | 51 | |
52 | 52 | // CreateBucketIfNotExists creates a new bucket if it doesn't already exist. |
53 | 53 | // Returns an error if the bucket name is blank, or if the bucket name is too long. |
54 | func (t *RWTransaction) CreateBucketIfNotExists(name string) error { | |
54 | func (t *RWTx) CreateBucketIfNotExists(name string) error { | |
55 | 55 | err := t.CreateBucket(name) |
56 | 56 | if err != nil && err != ErrBucketExists { |
57 | 57 | return err |
61 | 61 | |
62 | 62 | // DeleteBucket deletes a bucket. |
63 | 63 | // Returns an error if the bucket cannot be found. |
64 | func (t *RWTransaction) DeleteBucket(name string) error { | |
64 | func (t *RWTx) DeleteBucket(name string) error { | |
65 | 65 | b := t.Bucket(name) |
66 | 66 | if b == nil { |
67 | 67 | return ErrBucketNotFound |
80 | 80 | |
81 | 81 | // Commit writes all changes to disk and updates the meta page. |
82 | 82 | // Returns an error if a disk write error occurs. |
83 | func (t *RWTransaction) Commit() error { | |
83 | func (t *RWTx) Commit() error { | |
84 | 84 | if t.db == nil { |
85 | 85 | return nil |
86 | 86 | } |
117 | 117 | } |
118 | 118 | |
119 | 119 | // Rollback closes the transaction and ignores all previous updates. |
120 | func (t *RWTransaction) Rollback() { | |
120 | func (t *RWTx) Rollback() { | |
121 | 121 | t.close() |
122 | 122 | } |
123 | 123 | |
124 | func (t *RWTransaction) close() { | |
124 | func (t *RWTx) close() { | |
125 | 125 | if t.db != nil { |
126 | 126 | t.db.rwlock.Unlock() |
127 | 127 | t.db = nil |
129 | 129 | } |
130 | 130 | |
131 | 131 | // allocate returns a contiguous block of memory starting at a given page. |
132 | func (t *RWTransaction) allocate(count int) (*page, error) { | |
132 | func (t *RWTx) allocate(count int) (*page, error) { | |
133 | 133 | p, err := t.db.allocate(count) |
134 | 134 | if err != nil { |
135 | 135 | return nil, err |
142 | 142 | } |
143 | 143 | |
144 | 144 | // rebalance attempts to balance all nodes. |
145 | func (t *RWTransaction) rebalance() { | |
145 | func (t *RWTx) rebalance() { | |
146 | 146 | for _, n := range t.nodes { |
147 | 147 | n.rebalance() |
148 | 148 | } |
149 | 149 | } |
150 | 150 | |
151 | 151 | // spill writes all the nodes to dirty pages. |
152 | func (t *RWTransaction) spill() error { | |
152 | func (t *RWTx) spill() error { | |
153 | 153 | // Keep track of the current root nodes. |
154 | 154 | // We will update this at the end once all nodes are created. |
155 | 155 | type root struct { |
181 | 181 | |
182 | 182 | // If this is a root node that split then create a parent node. |
183 | 183 | if n.parent == nil && len(newNodes) > 1 { |
184 | n.parent = &node{transaction: t, isLeaf: false} | |
184 | n.parent = &node{tx: t, isLeaf: false} | |
185 | 185 | nodes = append(nodes, n.parent) |
186 | 186 | } |
187 | 187 | |
232 | 232 | } |
233 | 233 | |
234 | 234 | // write writes any dirty pages to disk. |
235 | func (t *RWTransaction) write() error { | |
235 | func (t *RWTx) write() error { | |
236 | 236 | // Sort pages by id. |
237 | 237 | pages := make(pages, 0, len(t.pages)) |
238 | 238 | for _, p := range t.pages { |
257 | 257 | } |
258 | 258 | |
259 | 259 | // writeMeta writes the meta to the disk. |
260 | func (t *RWTransaction) writeMeta() error { | |
260 | func (t *RWTx) writeMeta() error { | |
261 | 261 | // Create a temporary buffer for the meta page. |
262 | 262 | buf := make([]byte, t.db.pageSize) |
263 | 263 | p := t.db.pageInBuffer(buf, 0) |
270 | 270 | } |
271 | 271 | |
272 | 272 | // node creates a node from a page and associates it with a given parent. |
273 | func (t *RWTransaction) node(pgid pgid, parent *node) *node { | |
273 | func (t *RWTx) node(pgid pgid, parent *node) *node { | |
274 | 274 | // Retrieve node if it has already been fetched. |
275 | if n := t.Transaction.node(pgid); n != nil { | |
275 | if n := t.Tx.node(pgid); n != nil { | |
276 | 276 | return n |
277 | 277 | } |
278 | 278 | |
279 | 279 | // Otherwise create a branch and cache it. |
280 | n := &node{transaction: t, parent: parent} | |
280 | n := &node{tx: t, parent: parent} | |
281 | 281 | if n.parent != nil { |
282 | 282 | n.depth = n.parent.depth + 1 |
283 | 283 | } |
288 | 288 | } |
289 | 289 | |
290 | 290 | // dereference removes all references to the old mmap. |
291 | func (t *RWTransaction) dereference() { | |
291 | func (t *RWTx) dereference() { | |
292 | 292 | for _, n := range t.nodes { |
293 | 293 | n.dereference() |
294 | 294 | } |
8 | 8 | "github.com/stretchr/testify/assert" |
9 | 9 | ) |
10 | 10 | |
11 | // Ensure that a RWTransaction can be retrieved. | |
12 | func TestRWTransaction(t *testing.T) { | |
11 | // Ensure that a RWTx can be retrieved. | |
12 | func TestRWTx(t *testing.T) { | |
13 | 13 | withOpenDB(func(db *DB, path string) { |
14 | txn, err := db.RWTransaction() | |
14 | txn, err := db.RWTx() | |
15 | 15 | assert.NotNil(t, txn) |
16 | 16 | assert.NoError(t, err) |
17 | 17 | assert.Equal(t, txn.DB(), db) |
18 | 18 | }) |
19 | 19 | } |
20 | 20 | |
21 | // Ensure that opening a RWTransaction while the DB is closed returns an error. | |
22 | func TestRWTransactionOpenWithClosedDB(t *testing.T) { | |
21 | // Ensure that opening a RWTx while the DB is closed returns an error. | |
22 | func TestRWTxOpenWithClosedDB(t *testing.T) { | |
23 | 23 | withDB(func(db *DB, path string) { |
24 | txn, err := db.RWTransaction() | |
24 | txn, err := db.RWTx() | |
25 | 25 | assert.Equal(t, err, ErrDatabaseNotOpen) |
26 | 26 | assert.Nil(t, txn) |
27 | 27 | }) |
28 | 28 | } |
29 | 29 | |
30 | 30 | // Ensure that retrieving all buckets returns writable buckets. |
31 | func TestRWTransactionBuckets(t *testing.T) { | |
31 | func TestRWTxBuckets(t *testing.T) { | |
32 | 32 | withOpenDB(func(db *DB, path string) { |
33 | 33 | db.CreateBucket("widgets") |
34 | 34 | db.CreateBucket("woojits") |
35 | db.Do(func(txn *RWTransaction) error { | |
35 | db.Do(func(txn *RWTx) error { | |
36 | 36 | buckets := txn.Buckets() |
37 | 37 | assert.Equal(t, len(buckets), 2) |
38 | 38 | assert.Equal(t, buckets[0].Name(), "widgets") |
49 | 49 | } |
50 | 50 | |
51 | 51 | // Ensure that a bucket can be created and retrieved. |
52 | func TestRWTransactionCreateBucket(t *testing.T) { | |
52 | func TestRWTxCreateBucket(t *testing.T) { | |
53 | 53 | withOpenDB(func(db *DB, path string) { |
54 | 54 | // Create a bucket. |
55 | 55 | err := db.CreateBucket("widgets") |
63 | 63 | } |
64 | 64 | |
65 | 65 | // Ensure that a bucket can be created if it doesn't already exist. |
66 | func TestRWTransactionCreateBucketIfNotExists(t *testing.T) { | |
66 | func TestRWTxCreateBucketIfNotExists(t *testing.T) { | |
67 | 67 | withOpenDB(func(db *DB, path string) { |
68 | 68 | assert.NoError(t, db.CreateBucketIfNotExists("widgets")) |
69 | 69 | assert.NoError(t, db.CreateBucketIfNotExists("widgets")) |
77 | 77 | } |
78 | 78 | |
79 | 79 | // Ensure that a bucket cannot be created twice. |
80 | func TestRWTransactionRecreateBucket(t *testing.T) { | |
80 | func TestRWTxRecreateBucket(t *testing.T) { | |
81 | 81 | withOpenDB(func(db *DB, path string) { |
82 | 82 | // Create a bucket. |
83 | 83 | err := db.CreateBucket("widgets") |
90 | 90 | } |
91 | 91 | |
92 | 92 | // Ensure that a bucket is created with a non-blank name. |
93 | func TestRWTransactionCreateBucketWithoutName(t *testing.T) { | |
93 | func TestRWTxCreateBucketWithoutName(t *testing.T) { | |
94 | 94 | withOpenDB(func(db *DB, path string) { |
95 | 95 | err := db.CreateBucket("") |
96 | 96 | assert.Equal(t, err, ErrBucketNameRequired) |
98 | 98 | } |
99 | 99 | |
100 | 100 | // Ensure that a bucket name is not too long. |
101 | func TestRWTransactionCreateBucketWithLongName(t *testing.T) { | |
101 | func TestRWTxCreateBucketWithLongName(t *testing.T) { | |
102 | 102 | withOpenDB(func(db *DB, path string) { |
103 | 103 | err := db.CreateBucket(strings.Repeat("X", 255)) |
104 | 104 | assert.NoError(t, err) |
109 | 109 | } |
110 | 110 | |
111 | 111 | // Ensure that a bucket can be deleted. |
112 | func TestRWTransactionDeleteBucket(t *testing.T) { | |
112 | func TestRWTxDeleteBucket(t *testing.T) { | |
113 | 113 | withOpenDB(func(db *DB, path string) { |
114 | 114 | // Create a bucket and add a value. |
115 | 115 | db.CreateBucket("widgets") |
135 | 135 | } |
136 | 136 | |
137 | 137 | // Ensure that an error is returned when deleting from a bucket that doesn't exist. |
138 | func TestRWTransactionDeleteBucketNotFound(t *testing.T) { | |
138 | func TestRWTxDeleteBucketNotFound(t *testing.T) { | |
139 | 139 | withOpenDB(func(db *DB, path string) { |
140 | 140 | err := db.DeleteBucket("widgets") |
141 | 141 | assert.Equal(t, err, ErrBucketNotFound) |
143 | 143 | } |
144 | 144 | |
145 | 145 | // Benchmark the performance of bulk put transactions in random order. |
146 | func BenchmarkRWTransactionPutRandom(b *testing.B) { | |
146 | func BenchmarkRWTxPutRandom(b *testing.B) { | |
147 | 147 | indexes := rand.Perm(b.N) |
148 | 148 | value := []byte(strings.Repeat("0", 64)) |
149 | 149 | withOpenDB(func(db *DB, path string) { |
150 | 150 | db.CreateBucket("widgets") |
151 | var txn *RWTransaction | |
151 | var txn *RWTx | |
152 | 152 | var bucket *Bucket |
153 | 153 | for i := 0; i < b.N; i++ { |
154 | 154 | if i%1000 == 0 { |
155 | 155 | if txn != nil { |
156 | 156 | txn.Commit() |
157 | 157 | } |
158 | txn, _ = db.RWTransaction() | |
158 | txn, _ = db.RWTx() | |
159 | 159 | bucket = txn.Bucket("widgets") |
160 | 160 | } |
161 | 161 | bucket.Put([]byte(strconv.Itoa(indexes[i])), value) |
165 | 165 | } |
166 | 166 | |
167 | 167 | // Benchmark the performance of bulk put transactions in sequential order. |
168 | func BenchmarkRWTransactionPutSequential(b *testing.B) { | |
168 | func BenchmarkRWTxPutSequential(b *testing.B) { | |
169 | 169 | value := []byte(strings.Repeat("0", 64)) |
170 | 170 | withOpenDB(func(db *DB, path string) { |
171 | 171 | db.CreateBucket("widgets") |
172 | db.Do(func(txn *RWTransaction) error { | |
172 | db.Do(func(txn *RWTx) error { | |
173 | 173 | bucket := txn.Bucket("widgets") |
174 | 174 | for i := 0; i < b.N; i++ { |
175 | 175 | bucket.Put([]byte(strconv.Itoa(i)), value) |
0 | 0 | package bolt |
1 | 1 | |
2 | // Transaction represents a read-only transaction on the database. | |
2 | // Tx represents a read-only transaction on the database. | |
3 | 3 | // It can be used for retrieving values for keys as well as creating cursors for |
4 | 4 | // iterating over the data. |
5 | 5 | // |
6 | 6 | // IMPORTANT: You must close transactions when you are done with them. Pages |
7 | 7 | // can not be reclaimed by the writer until no more transactions are using them. |
8 | 8 | // A long running read transaction can cause the database to quickly grow. |
9 | type Transaction struct { | |
10 | db *DB | |
11 | rwtransaction *RWTransaction | |
12 | meta *meta | |
13 | buckets *buckets | |
14 | nodes map[pgid]*node | |
15 | pages map[pgid]*page | |
9 | type Tx struct { | |
10 | db *DB | |
11 | rwtx *RWTx | |
12 | meta *meta | |
13 | buckets *buckets | |
14 | nodes map[pgid]*node | |
15 | pages map[pgid]*page | |
16 | 16 | } |
17 | 17 | |
18 | // txnid represents the internal transaction identifier. | |
19 | type txnid uint64 | |
18 | // txid represents the internal transaction identifier. | |
19 | type txid uint64 | |
20 | 20 | |
21 | 21 | // init initializes the transaction and associates it with a database. |
22 | func (t *Transaction) init(db *DB) { | |
22 | func (t *Tx) init(db *DB) { | |
23 | 23 | t.db = db |
24 | 24 | t.pages = nil |
25 | 25 | |
33 | 33 | } |
34 | 34 | |
35 | 35 | // id returns the transaction id. |
36 | func (t *Transaction) id() txnid { | |
37 | return t.meta.txnid | |
36 | func (t *Tx) id() txid { | |
37 | return t.meta.txid | |
38 | 38 | } |
39 | 39 | |
40 | 40 | // Close closes the transaction and releases any pages it is using. |
41 | func (t *Transaction) Close() { | |
41 | func (t *Tx) Close() { | |
42 | 42 | if t.db != nil { |
43 | if t.rwtransaction != nil { | |
44 | t.rwtransaction.Rollback() | |
43 | if t.rwtx != nil { | |
44 | t.rwtx.Rollback() | |
45 | 45 | } else { |
46 | t.db.removeTransaction(t) | |
46 | t.db.removeTx(t) | |
47 | 47 | t.db = nil |
48 | 48 | } |
49 | 49 | } |
50 | 50 | } |
51 | 51 | |
52 | 52 | // DB returns a reference to the database that created the transaction. |
53 | func (t *Transaction) DB() *DB { | |
53 | func (t *Tx) DB() *DB { | |
54 | 54 | return t.db |
55 | 55 | } |
56 | 56 | |
57 | 57 | // Bucket retrieves a bucket by name. |
58 | 58 | // Returns nil if the bucket does not exist. |
59 | func (t *Transaction) Bucket(name string) *Bucket { | |
59 | func (t *Tx) Bucket(name string) *Bucket { | |
60 | 60 | b := t.buckets.get(name) |
61 | 61 | if b == nil { |
62 | 62 | return nil |
63 | 63 | } |
64 | 64 | |
65 | 65 | return &Bucket{ |
66 | bucket: b, | |
67 | name: name, | |
68 | transaction: t, | |
69 | rwtransaction: t.rwtransaction, | |
66 | bucket: b, | |
67 | name: name, | |
68 | tx: t, | |
69 | rwtx: t.rwtx, | |
70 | 70 | } |
71 | 71 | } |
72 | 72 | |
73 | 73 | // Buckets retrieves a list of all buckets. |
74 | func (t *Transaction) Buckets() []*Bucket { | |
74 | func (t *Tx) Buckets() []*Bucket { | |
75 | 75 | buckets := make([]*Bucket, 0, len(t.buckets.items)) |
76 | 76 | for name, b := range t.buckets.items { |
77 | 77 | bucket := &Bucket{ |
78 | bucket: b, | |
79 | name: name, | |
80 | transaction: t, | |
81 | rwtransaction: t.rwtransaction, | |
78 | bucket: b, | |
79 | name: name, | |
80 | tx: t, | |
81 | rwtx: t.rwtx, | |
82 | 82 | } |
83 | 83 | buckets = append(buckets, bucket) |
84 | 84 | } |
87 | 87 | |
88 | 88 | // page returns a reference to the page with a given id. |
89 | 89 | // If page has been written to then a temporary bufferred page is returned. |
90 | func (t *Transaction) page(id pgid) *page { | |
90 | func (t *Tx) page(id pgid) *page { | |
91 | 91 | // Check the dirty pages first. |
92 | 92 | if t.pages != nil { |
93 | 93 | if p, ok := t.pages[id]; ok { |
100 | 100 | } |
101 | 101 | |
102 | 102 | // node returns a reference to the in-memory node for a given page, if it exists. |
103 | func (t *Transaction) node(id pgid) *node { | |
103 | func (t *Tx) node(id pgid) *node { | |
104 | 104 | if t.nodes == nil { |
105 | 105 | return nil |
106 | 106 | } |
109 | 109 | |
110 | 110 | // pageNode returns the in-memory node, if it exists. |
111 | 111 | // Otherwise returns the underlying page. |
112 | func (t *Transaction) pageNode(id pgid) (*page, *node) { | |
112 | func (t *Tx) pageNode(id pgid) (*page, *node) { | |
113 | 113 | if n := t.node(id); n != nil { |
114 | 114 | return nil, n |
115 | 115 | } |
117 | 117 | } |
118 | 118 | |
119 | 119 | // forEachPage iterates over every page within a given page and executes a function. |
120 | func (t *Transaction) forEachPage(pgid pgid, depth int, fn func(*page, int)) { | |
120 | func (t *Tx) forEachPage(pgid pgid, depth int, fn func(*page, int)) { | |
121 | 121 | p := t.page(pgid) |
122 | 122 | |
123 | 123 | // Execute function. |
13 | 13 | ) |
14 | 14 | |
15 | 15 | // Ensure that the database can retrieve a list of buckets. |
16 | func TestTransactionBuckets(t *testing.T) { | |
16 | func TestTxBuckets(t *testing.T) { | |
17 | 17 | withOpenDB(func(db *DB, path string) { |
18 | 18 | db.CreateBucket("foo") |
19 | 19 | db.CreateBucket("bar") |
27 | 27 | }) |
28 | 28 | } |
29 | 29 | |
30 | // Ensure that a Transaction can retrieve a bucket. | |
31 | func TestTransactionBucketMissing(t *testing.T) { | |
30 | // Ensure that a Tx can retrieve a bucket. | |
31 | func TestTxBucketMissing(t *testing.T) { | |
32 | 32 | withOpenDB(func(db *DB, path string) { |
33 | 33 | db.CreateBucket("widgets") |
34 | 34 | b, err := db.Bucket("widgets") |
39 | 39 | }) |
40 | 40 | } |
41 | 41 | |
42 | // Ensure that a Transaction retrieving a non-existent key returns nil. | |
43 | func TestTransactionGetMissing(t *testing.T) { | |
42 | // Ensure that a Tx retrieving a non-existent key returns nil. | |
43 | func TestTxGetMissing(t *testing.T) { | |
44 | 44 | withOpenDB(func(db *DB, path string) { |
45 | 45 | db.CreateBucket("widgets") |
46 | 46 | db.Put("widgets", []byte("foo"), []byte("bar")) |
50 | 50 | }) |
51 | 51 | } |
52 | 52 | |
53 | // Ensure that a Transaction cursor can iterate over an empty bucket without error. | |
54 | func TestTransactionCursorEmptyBucket(t *testing.T) { | |
55 | withOpenDB(func(db *DB, path string) { | |
56 | db.CreateBucket("widgets") | |
57 | txn, _ := db.Transaction() | |
53 | // Ensure that a Tx cursor can iterate over an empty bucket without error. | |
54 | func TestTxCursorEmptyBucket(t *testing.T) { | |
55 | withOpenDB(func(db *DB, path string) { | |
56 | db.CreateBucket("widgets") | |
57 | txn, _ := db.Tx() | |
58 | 58 | c := txn.Bucket("widgets").Cursor() |
59 | 59 | k, v := c.First() |
60 | 60 | assert.Nil(t, k) |
63 | 63 | }) |
64 | 64 | } |
65 | 65 | |
66 | // Ensure that a Transaction cursor can iterate over a single root with a couple elements. | |
67 | func TestTransactionCursorLeafRoot(t *testing.T) { | |
66 | // Ensure that a Tx cursor can iterate over a single root with a couple elements. | |
67 | func TestTxCursorLeafRoot(t *testing.T) { | |
68 | 68 | withOpenDB(func(db *DB, path string) { |
69 | 69 | db.CreateBucket("widgets") |
70 | 70 | db.Put("widgets", []byte("baz"), []byte{}) |
71 | 71 | db.Put("widgets", []byte("foo"), []byte{0}) |
72 | 72 | db.Put("widgets", []byte("bar"), []byte{1}) |
73 | txn, _ := db.Transaction() | |
73 | txn, _ := db.Tx() | |
74 | 74 | c := txn.Bucket("widgets").Cursor() |
75 | 75 | |
76 | 76 | k, v := c.First() |
97 | 97 | }) |
98 | 98 | } |
99 | 99 | |
100 | // Ensure that a Transaction cursor can iterate in reverse over a single root with a couple elements. | |
101 | func TestTransactionCursorLeafRootReverse(t *testing.T) { | |
100 | // Ensure that a Tx cursor can iterate in reverse over a single root with a couple elements. | |
101 | func TestTxCursorLeafRootReverse(t *testing.T) { | |
102 | 102 | withOpenDB(func(db *DB, path string) { |
103 | 103 | db.CreateBucket("widgets") |
104 | 104 | db.Put("widgets", []byte("baz"), []byte{}) |
105 | 105 | db.Put("widgets", []byte("foo"), []byte{0}) |
106 | 106 | db.Put("widgets", []byte("bar"), []byte{1}) |
107 | txn, _ := db.Transaction() | |
107 | txn, _ := db.Tx() | |
108 | 108 | c := txn.Bucket("widgets").Cursor() |
109 | 109 | |
110 | 110 | k, v := c.Last() |
131 | 131 | }) |
132 | 132 | } |
133 | 133 | |
134 | // Ensure that a Transaction cursor can restart from the beginning. | |
135 | func TestTransactionCursorRestart(t *testing.T) { | |
134 | // Ensure that a Tx cursor can restart from the beginning. | |
135 | func TestTxCursorRestart(t *testing.T) { | |
136 | 136 | withOpenDB(func(db *DB, path string) { |
137 | 137 | db.CreateBucket("widgets") |
138 | 138 | db.Put("widgets", []byte("bar"), []byte{}) |
139 | 139 | db.Put("widgets", []byte("foo"), []byte{}) |
140 | 140 | |
141 | txn, _ := db.Transaction() | |
141 | txn, _ := db.Tx() | |
142 | 142 | c := txn.Bucket("widgets").Cursor() |
143 | 143 | |
144 | 144 | k, _ := c.First() |
157 | 157 | }) |
158 | 158 | } |
159 | 159 | |
160 | // Ensure that a transaction can iterate over all elements in a bucket. | |
161 | func TestTransactionCursorIterate(t *testing.T) { | |
160 | // Ensure that a Tx can iterate over all elements in a bucket. | |
161 | func TestTxCursorIterate(t *testing.T) { | |
162 | 162 | f := func(items testdata) bool { |
163 | 163 | withOpenDB(func(db *DB, path string) { |
164 | 164 | // Bulk insert all values. |
165 | 165 | db.CreateBucket("widgets") |
166 | rwtxn, _ := db.RWTransaction() | |
166 | rwtxn, _ := db.RWTx() | |
167 | 167 | b := rwtxn.Bucket("widgets") |
168 | 168 | for _, item := range items { |
169 | 169 | assert.NoError(t, b.Put(item.Key, item.Value)) |
175 | 175 | |
176 | 176 | // Iterate over all items and check consistency. |
177 | 177 | var index = 0 |
178 | txn, _ := db.Transaction() | |
178 | txn, _ := db.Tx() | |
179 | 179 | c := txn.Bucket("widgets").Cursor() |
180 | 180 | for k, v := c.First(); k != nil && index < len(items); k, v = c.Next() { |
181 | 181 | assert.Equal(t, k, items[index].Key) |
195 | 195 | } |
196 | 196 | |
197 | 197 | // Ensure that a transaction can iterate over all elements in a bucket in reverse. |
198 | func TestTransactionCursorIterateReverse(t *testing.T) { | |
198 | func TestTxCursorIterateReverse(t *testing.T) { | |
199 | 199 | f := func(items testdata) bool { |
200 | 200 | withOpenDB(func(db *DB, path string) { |
201 | 201 | // Bulk insert all values. |
202 | 202 | db.CreateBucket("widgets") |
203 | rwtxn, _ := db.RWTransaction() | |
203 | rwtxn, _ := db.RWTx() | |
204 | 204 | b := rwtxn.Bucket("widgets") |
205 | 205 | for _, item := range items { |
206 | 206 | assert.NoError(t, b.Put(item.Key, item.Value)) |
212 | 212 | |
213 | 213 | // Iterate over all items and check consistency. |
214 | 214 | var index = 0 |
215 | txn, _ := db.Transaction() | |
215 | txn, _ := db.Tx() | |
216 | 216 | c := txn.Bucket("widgets").Cursor() |
217 | 217 | for k, v := c.Last(); k != nil && index < len(items); k, v = c.Prev() { |
218 | 218 | assert.Equal(t, k, items[index].Key) |
232 | 232 | } |
233 | 233 | |
234 | 234 | // Benchmark the performance iterating over a cursor. |
235 | func BenchmarkTransactionCursor(b *testing.B) { | |
235 | func BenchmarkTxCursor(b *testing.B) { | |
236 | 236 | indexes := rand.Perm(b.N) |
237 | 237 | value := []byte(strings.Repeat("0", 64)) |
238 | 238 | |
239 | 239 | withOpenDB(func(db *DB, path string) { |
240 | 240 | // Write data to bucket. |
241 | 241 | db.CreateBucket("widgets") |
242 | db.Do(func(txn *RWTransaction) error { | |
242 | db.Do(func(txn *RWTx) error { | |
243 | 243 | bucket := txn.Bucket("widgets") |
244 | 244 | for i := 0; i < b.N; i++ { |
245 | 245 | bucket.Put([]byte(strconv.Itoa(indexes[i])), value) |
249 | 249 | b.ResetTimer() |
250 | 250 | |
251 | 251 | // Iterate over bucket using cursor. |
252 | db.With(func(txn *Transaction) error { | |
252 | db.With(func(txn *Tx) error { | |
253 | 253 | count := 0 |
254 | 254 | c := txn.Bucket("widgets").Cursor() |
255 | 255 | for k, _ := c.First(); k != nil; k, _ = c.Next() { |