bolt/db.go

604 lines
16 KiB
Go
Raw Normal View History

2014-01-08 15:06:17 +00:00
package bolt
2014-01-12 05:51:01 +00:00
import (
2014-02-16 19:11:10 +00:00
"fmt"
2014-01-31 18:18:51 +00:00
"io"
2014-01-12 22:30:09 +00:00
"os"
2014-01-12 05:51:01 +00:00
"sync"
"syscall"
"unsafe"
)
2014-02-12 21:57:27 +00:00
// The smallest size that the mmap can be.
2014-02-11 19:16:12 +00:00
const minMmapSize = 1 << 22 // 4MB
2014-02-12 21:57:27 +00:00
// The largest step that can be taken when remapping the mmap.
2014-02-11 19:16:12 +00:00
const maxMmapStep = 1 << 30 // 1GB
2014-02-12 21:57:27 +00:00
// DB represents a collection of buckets persisted to a file on disk.
// All data access is performed through transactions which can be obtained through the DB.
// All the functions on DB will return a ErrDatabaseNotOpen if accessed before Open() is called.
2014-01-12 05:51:01 +00:00
type DB struct {
2014-01-24 23:32:18 +00:00
os _os
syscall _syscall
path string
file file
metafile file
data []byte
meta0 *meta
meta1 *meta
pageSize int
2014-02-09 22:52:19 +00:00
opened bool
rwtransaction *RWTransaction
2014-01-24 23:32:18 +00:00
transactions []*Transaction
2014-02-09 22:52:19 +00:00
freelist *freelist
rwlock sync.Mutex // Allows only one writer at a time.
metalock sync.Mutex // Protects meta page access.
mmaplock sync.RWMutex // Protects mmap access during remapping.
2014-01-12 05:51:01 +00:00
}
2014-01-24 19:51:56 +00:00
// Path returns the path to currently open database file.
2014-01-12 05:51:01 +00:00
func (db *DB) Path() string {
2014-01-09 16:07:10 +00:00
return db.path
}
// GoString returns the Go string representation of the database.
func (db *DB) GoString() string {
return fmt.Sprintf("bolt.DB{path:%q}", db.path)
}
// String returns the string representation of the database.
func (db *DB) String() string {
return fmt.Sprintf("DB<%q>", db.path)
}
2014-01-24 19:51:56 +00:00
// Open opens a data file at the given path and initializes the database.
// If the file does not exist then it will be created automatically.
2014-01-12 22:30:09 +00:00
func (db *DB) Open(path string, mode os.FileMode) error {
2014-01-10 14:32:12 +00:00
var err error
2014-02-09 22:52:19 +00:00
db.metalock.Lock()
defer db.metalock.Unlock()
2014-01-10 14:32:12 +00:00
2014-01-24 19:51:56 +00:00
// Initialize OS/Syscall references.
// These are overridden by mocks during some tests.
2014-01-12 21:59:03 +00:00
if db.os == nil {
db.os = &sysos{}
}
2014-01-12 22:50:35 +00:00
if db.syscall == nil {
db.syscall = &syssyscall{}
}
2014-01-12 21:59:03 +00:00
2014-01-10 14:32:12 +00:00
// Exit if the database is currently open.
if db.opened {
2014-02-16 19:18:44 +00:00
return ErrDatabaseOpen
2014-01-10 14:32:12 +00:00
}
// Open data file and separate sync handler for metadata writes.
db.path = path
2014-01-12 22:30:09 +00:00
if db.file, err = db.os.OpenFile(db.path, os.O_RDWR|os.O_CREATE, mode); err != nil {
2014-01-10 14:32:12 +00:00
db.close()
return err
}
2014-01-12 22:30:09 +00:00
if db.metafile, err = db.os.OpenFile(db.path, os.O_RDWR|os.O_SYNC, mode); err != nil {
2014-01-10 14:32:12 +00:00
db.close()
return err
}
2014-01-28 19:52:09 +00:00
// Initialize the database if it doesn't exist.
if info, err := db.file.Stat(); err != nil {
return &Error{"stat error", err}
} else if info.Size() == 0 {
// Initialize new files with meta pages.
2014-01-12 05:51:01 +00:00
if err := db.init(); err != nil {
return err
2014-01-10 14:32:12 +00:00
}
2014-01-28 19:52:09 +00:00
} else {
// Read the first meta page to determine the page size.
2014-02-12 21:57:27 +00:00
var buf [0x1000]byte
2014-01-28 19:52:09 +00:00
if _, err := db.file.ReadAt(buf[:], 0); err == nil {
2014-01-30 03:35:58 +00:00
m := db.pageInBuffer(buf[:], 0).meta()
if err := m.validate(); err != nil {
return &Error{"meta error", err}
2014-01-28 19:52:09 +00:00
}
2014-01-30 03:35:58 +00:00
db.pageSize = int(m.pageSize)
2014-01-28 19:52:09 +00:00
}
2014-01-10 14:32:12 +00:00
}
// Memory map the data file.
2014-02-11 19:16:12 +00:00
if err := db.mmap(0); err != nil {
2014-01-10 14:32:12 +00:00
db.close()
return err
}
2014-02-09 22:52:19 +00:00
// Read in the freelist.
db.freelist = &freelist{pending: make(map[txnid][]pgid)}
db.freelist.read(db.page(db.meta().freelist))
2014-01-10 14:32:12 +00:00
// Mark the database as opened and return.
db.opened = true
return nil
}
2014-01-24 19:51:56 +00:00
// mmap opens the underlying memory-mapped file and initializes the meta references.
2014-02-11 19:16:12 +00:00
// minsz is the minimum size that the new mmap can be.
func (db *DB) mmap(minsz int) error {
db.mmaplock.Lock()
defer db.mmaplock.Unlock()
// Dereference all mmap references before unmapping.
if db.rwtransaction != nil {
db.rwtransaction.dereference()
}
// Unmap existing data before continuing.
db.munmap()
2014-01-28 19:52:09 +00:00
info, err := db.file.Stat()
if err != nil {
return &Error{"mmap stat error", err}
2014-01-28 20:16:22 +00:00
} else if int(info.Size()) < db.pageSize*2 {
2014-01-28 19:52:09 +00:00
return &Error{"file size too small", err}
2014-01-10 14:32:12 +00:00
}
2014-01-30 23:22:02 +00:00
2014-02-11 19:16:12 +00:00
// Ensure the size is at least the minimum size.
var size = int(info.Size())
if size < minsz {
size = minsz
}
size = db.mmapSize(minsz)
2014-01-10 14:32:12 +00:00
2014-01-12 05:51:01 +00:00
// Memory-map the data file as a byte slice.
2014-01-12 22:50:35 +00:00
if db.data, err = db.syscall.Mmap(int(db.file.Fd()), 0, size, syscall.PROT_READ, syscall.MAP_SHARED); err != nil {
2014-01-12 05:51:01 +00:00
return err
}
2014-01-10 14:32:12 +00:00
2014-01-12 05:51:01 +00:00
// Save references to the meta pages.
2014-01-30 03:35:58 +00:00
db.meta0 = db.page(0).meta()
db.meta1 = db.page(1).meta()
// Validate the meta pages.
if err := db.meta0.validate(); err != nil {
2014-01-12 05:51:01 +00:00
return &Error{"meta0 error", err}
2014-01-10 14:32:12 +00:00
}
2014-01-30 03:35:58 +00:00
if err := db.meta1.validate(); err != nil {
2014-01-12 05:51:01 +00:00
return &Error{"meta1 error", err}
2014-01-10 14:32:12 +00:00
}
return nil
}
2014-02-11 19:16:12 +00:00
// munmap unmaps the data file from memory.
func (db *DB) munmap() {
if db.data != nil {
if err := db.syscall.Munmap(db.data); err != nil {
panic("unmap error: " + err.Error())
}
db.data = nil
}
}
// mmapSize determines the appropriate size for the mmap given the current size
// of the database. The minimum size is 4MB and doubles until it reaches 1GB.
func (db *DB) mmapSize(size int) int {
if size < minMmapSize {
return minMmapSize
} else if size < maxMmapStep {
size *= 2
} else {
size += maxMmapStep
}
// Ensure that the mmap size is a multiple of the page size.
if (size % db.pageSize) != 0 {
size = ((size / db.pageSize) + 1) * db.pageSize
}
return size
}
2014-01-12 05:51:01 +00:00
// init creates a new database file and initializes its meta pages.
func (db *DB) init() error {
2014-01-28 03:22:37 +00:00
// Set the page size to the OS page size.
2014-01-12 21:59:03 +00:00
db.pageSize = db.os.Getpagesize()
2014-01-10 14:32:12 +00:00
2014-01-12 05:51:01 +00:00
// Create two meta pages on a buffer.
2014-01-30 03:50:29 +00:00
buf := make([]byte, db.pageSize*4)
2014-01-12 05:51:01 +00:00
for i := 0; i < 2; i++ {
2014-01-27 15:11:54 +00:00
p := db.pageInBuffer(buf[:], pgid(i))
p.id = pgid(i)
2014-02-12 21:57:27 +00:00
p.flags = metaPageFlag
2014-01-30 03:35:58 +00:00
2014-01-30 03:50:29 +00:00
// Initialize the meta page.
2014-01-30 03:35:58 +00:00
m := p.meta()
m.magic = magic
2014-01-31 17:22:58 +00:00
m.version = version
2014-01-30 03:35:58 +00:00
m.pageSize = uint32(db.pageSize)
2014-01-31 17:22:58 +00:00
m.version = version
2014-02-09 22:52:19 +00:00
m.freelist = 2
2014-02-06 05:15:47 +00:00
m.buckets = 3
2014-01-30 23:22:02 +00:00
m.pgid = 4
m.txnid = txnid(i)
2014-01-12 05:51:01 +00:00
}
2014-01-10 14:32:12 +00:00
2014-01-30 03:50:29 +00:00
// Write an empty freelist at page 3.
p := db.pageInBuffer(buf[:], pgid(2))
2014-01-30 05:11:46 +00:00
p.id = pgid(2)
2014-02-12 21:57:27 +00:00
p.flags = freelistPageFlag
2014-01-30 03:50:29 +00:00
p.count = 0
// Write an empty leaf page at page 4.
p = db.pageInBuffer(buf[:], pgid(3))
2014-01-30 05:11:46 +00:00
p.id = pgid(3)
2014-02-12 21:57:27 +00:00
p.flags = bucketsPageFlag
2014-01-30 03:50:29 +00:00
p.count = 0
2014-01-12 05:51:01 +00:00
// Write the buffer to our data file.
if _, err := db.metafile.WriteAt(buf, 0); err != nil {
return err
}
2014-01-10 14:32:12 +00:00
2014-01-12 05:51:01 +00:00
return nil
}
2014-01-10 14:32:12 +00:00
2014-02-12 21:57:27 +00:00
// Close releases all database resources.
// All transactions must be closed before closing the database.
2014-01-24 19:51:56 +00:00
func (db *DB) Close() {
2014-02-09 22:52:19 +00:00
db.metalock.Lock()
defer db.metalock.Unlock()
2014-01-27 15:11:54 +00:00
db.close()
2014-01-24 19:51:56 +00:00
}
2014-01-12 05:51:01 +00:00
func (db *DB) close() {
2014-02-16 05:34:21 +00:00
db.opened = false
2014-02-16 06:38:03 +00:00
2014-02-11 19:16:12 +00:00
// TODO(benbjohnson): Undo everything in Open().
2014-02-09 22:52:19 +00:00
db.freelist = nil
2014-02-12 21:57:27 +00:00
db.path = ""
2014-02-11 19:16:12 +00:00
db.munmap()
2014-01-12 05:51:01 +00:00
}
2014-01-10 14:32:12 +00:00
2014-01-24 19:51:56 +00:00
// Transaction creates a read-only transaction.
// Multiple read-only transactions can be used concurrently.
2014-02-12 21:57:27 +00:00
//
// IMPORTANT: You must close the transaction after you are finished or else the database will not reclaim old pages.
2014-01-24 19:51:56 +00:00
func (db *DB) Transaction() (*Transaction, error) {
2014-02-09 22:52:19 +00:00
db.metalock.Lock()
defer db.metalock.Unlock()
2014-01-13 17:35:04 +00:00
2014-02-11 19:16:12 +00:00
// Obtain a read-only lock on the mmap. When the mmap is remapped it will
// obtain a write lock so all transactions must finish before it can be
// remapped.
db.mmaplock.RLock()
2014-01-13 17:35:04 +00:00
// Exit if the database is not open yet.
if !db.opened {
2014-02-16 19:18:44 +00:00
return nil, ErrDatabaseNotOpen
2014-01-13 17:35:04 +00:00
}
// Create a transaction associated with the database.
2014-01-26 22:29:06 +00:00
t := &Transaction{}
2014-01-30 23:22:02 +00:00
t.init(db)
2014-01-14 20:01:02 +00:00
2014-02-09 22:52:19 +00:00
// Keep track of transaction until it closes.
db.transactions = append(db.transactions, t)
2014-01-24 19:51:56 +00:00
return t, nil
}
// RWTransaction creates a read/write transaction.
// Only one read/write transaction is allowed at a time.
// You must call Commit() or Rollback() on the transaction to close it.
func (db *DB) RWTransaction() (*RWTransaction, error) {
2014-02-09 22:52:19 +00:00
db.metalock.Lock()
defer db.metalock.Unlock()
2014-01-26 22:29:06 +00:00
// Obtain writer lock. This is released by the RWTransaction when it closes.
2014-02-09 22:52:19 +00:00
db.rwlock.Lock()
2014-01-24 19:51:56 +00:00
2014-01-26 22:29:06 +00:00
// Exit if the database is not open yet.
if !db.opened {
2014-02-09 22:52:19 +00:00
db.rwlock.Unlock()
2014-02-16 19:18:44 +00:00
return nil, ErrDatabaseNotOpen
2014-01-13 17:35:04 +00:00
}
// Create a transaction associated with the database.
t := &RWTransaction{nodes: make(map[pgid]*node)}
2014-01-30 23:22:02 +00:00
t.init(db)
2014-02-09 22:52:19 +00:00
db.rwtransaction = t
// Free any pages associated with closed read-only transactions.
var minid txnid = 0xFFFFFFFFFFFFFFFF
for _, t := range db.transactions {
if t.id() < minid {
minid = t.id()
}
}
if minid > 0 {
db.freelist.release(minid - 1)
}
2014-01-26 22:29:06 +00:00
2014-01-13 17:35:04 +00:00
return t, nil
}
2014-02-09 22:52:19 +00:00
// removeTransaction removes a transaction from the database.
func (db *DB) removeTransaction(t *Transaction) {
db.metalock.Lock()
defer db.metalock.Unlock()
2014-02-11 19:16:12 +00:00
// Release the read lock on the mmap.
db.mmaplock.RUnlock()
2014-02-09 22:52:19 +00:00
// Remove the transaction.
for i, txn := range db.transactions {
if txn == t {
db.transactions = append(db.transactions[:i], db.transactions[i+1:]...)
break
}
}
}
// Do executes a function within the context of a RWTransaction.
2014-02-15 21:54:45 +00:00
// If no error is returned from the function then the transaction is committed.
// If an error is returned then the entire transaction is rolled back.
// Any error that is returned from the function or returned from the commit is
// returned from the Do() method.
func (db *DB) Do(fn func(*RWTransaction) error) error {
2014-02-15 21:54:45 +00:00
t, err := db.RWTransaction()
if err != nil {
return err
}
// If an error is returned from the function then rollback and return error.
if err := fn(t); err != nil {
t.Rollback()
return err
}
return t.Commit()
}
2014-02-16 22:43:35 +00:00
// With executes a function within the context of a Transaction.
// Any error that is returned from the function is returned from the With() method.
func (db *DB) With(fn func(*Transaction) error) error {
2014-02-16 20:51:35 +00:00
t, err := db.Transaction()
if err != nil {
return err
}
defer t.Close()
2014-02-16 22:43:35 +00:00
// If an error is returned from the function then pass it through.
return fn(t)
}
// ForEach executes a function for each key/value pair in a bucket.
// An error is returned if the bucket cannot be found.
func (db *DB) ForEach(name string, fn func(k, v []byte) error) error {
return db.With(func(t *Transaction) error {
return t.ForEach(name, fn)
2014-02-16 22:43:35 +00:00
})
2014-02-16 20:51:35 +00:00
}
2014-01-31 17:22:58 +00:00
// Bucket retrieves a reference to a bucket.
2014-02-12 21:57:27 +00:00
// This is typically useful for checking the existence of a bucket.
2014-01-31 17:22:58 +00:00
func (db *DB) Bucket(name string) (*Bucket, error) {
t, err := db.Transaction()
if err != nil {
return nil, err
}
defer t.Close()
2014-01-31 17:22:58 +00:00
return t.Bucket(name), nil
}
// Buckets retrieves a list of all buckets in the database.
func (db *DB) Buckets() ([]*Bucket, error) {
t, err := db.Transaction()
if err != nil {
return nil, err
}
defer t.Close()
2014-01-31 17:22:58 +00:00
return t.Buckets(), nil
}
2014-02-12 21:57:27 +00:00
// CreateBucket creates a new bucket with the given name.
// This function can return an error if the bucket already exists, if the name
// is blank, or the bucket name is too long.
2014-01-31 17:22:58 +00:00
func (db *DB) CreateBucket(name string) error {
return db.Do(func(t *RWTransaction) error {
2014-02-15 21:54:45 +00:00
return t.CreateBucket(name)
})
2014-01-31 17:22:58 +00:00
}
2014-02-16 19:36:37 +00:00
// CreateBucketIfNotExists creates a new bucket with the given name if it doesn't already exist.
// This function can return an error if the name is blank, or the bucket name is too long.
func (db *DB) CreateBucketIfNotExists(name string) error {
return db.Do(func(t *RWTransaction) error {
2014-02-16 19:36:37 +00:00
return t.CreateBucketIfNotExists(name)
})
}
2014-01-31 17:22:58 +00:00
// DeleteBucket removes a bucket from the database.
2014-02-12 21:57:27 +00:00
// Returns an error if the bucket does not exist.
2014-01-31 17:22:58 +00:00
func (db *DB) DeleteBucket(name string) error {
return db.Do(func(t *RWTransaction) error {
2014-02-15 21:54:45 +00:00
return t.DeleteBucket(name)
})
2014-01-31 17:22:58 +00:00
}
2014-02-15 17:23:00 +00:00
// NextSequence returns an autoincrementing integer for the bucket.
// This function can return an error if the bucket does not exist.
func (db *DB) NextSequence(name string) (int, error) {
var seq int
err := db.Do(func(t *RWTransaction) error {
2014-02-15 21:54:45 +00:00
var err error
seq, err = t.NextSequence(name)
return err
2014-02-15 21:54:45 +00:00
})
2014-02-15 17:23:00 +00:00
if err != nil {
return 0, err
}
return seq, nil
}
2014-01-31 17:22:58 +00:00
// Get retrieves the value for a key in a bucket.
// Returns an error if the key does not exist.
2014-01-31 17:22:58 +00:00
func (db *DB) Get(name string, key []byte) ([]byte, error) {
t, err := db.Transaction()
if err != nil {
return nil, err
}
defer t.Close()
return t.Get(name, key)
2014-01-31 17:22:58 +00:00
}
// Put sets the value for a key in a bucket.
2014-02-12 21:57:27 +00:00
// 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.
2014-01-31 17:22:58 +00:00
func (db *DB) Put(name string, key []byte, value []byte) error {
return db.Do(func(t *RWTransaction) error {
return t.Put(name, key, value)
2014-02-15 21:54:45 +00:00
})
2014-01-31 17:22:58 +00:00
}
// Delete removes a key from a bucket.
2014-02-12 21:57:27 +00:00
// Returns an error if the bucket cannot be found.
2014-01-31 17:22:58 +00:00
func (db *DB) Delete(name string, key []byte) error {
return db.Do(func(t *RWTransaction) error {
return t.Delete(name, key)
2014-02-15 21:54:45 +00:00
})
2014-01-31 17:22:58 +00:00
}
2014-01-31 18:18:51 +00:00
// Copy writes the entire database to a writer.
2014-02-12 21:57:27 +00:00
// A reader transaction is maintained during the copy so it is safe to continue
// using the database while a copy is in progress.
2014-01-31 18:18:51 +00:00
func (db *DB) Copy(w io.Writer) error {
// Maintain a reader transaction so pages don't get reclaimed.
t, err := db.Transaction()
if err != nil {
return err
}
defer t.Close()
2014-01-31 18:18:51 +00:00
// Open reader on the database.
f, err := os.Open(db.path)
if err != nil {
return err
}
defer f.Close()
// Copy everything.
if _, err := io.Copy(w, f); err != nil {
return err
}
return nil
}
// CopyFile copies the entire database to file at the given path.
2014-02-12 21:57:27 +00:00
// A reader transaction is maintained during the copy so it is safe to continue
// using the database while a copy is in progress.
2014-02-14 15:32:42 +00:00
func (db *DB) CopyFile(path string, mode os.FileMode) error {
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
2014-01-31 18:18:51 +00:00
if err != nil {
return err
}
defer f.Close()
return db.Copy(f)
}
2014-02-21 16:49:15 +00:00
// Stat retrieves stats on the database and its page usage.
// Returns an error if the database is not open.
func (db *DB) Stat() (*Stat, error) {
// Obtain meta & mmap locks.
db.metalock.Lock()
db.mmaplock.RLock()
var s = &Stat{
MmapSize: len(db.data),
TransactionCount: len(db.transactions),
}
// Release locks.
db.mmaplock.RUnlock()
db.metalock.Unlock()
err := db.Do(func(t *RWTransaction) error {
2014-02-21 16:49:15 +00:00
s.PageCount = int(t.meta.pgid)
s.FreePageCount = len(db.freelist.all())
s.PageSize = db.pageSize
return nil
})
if err != nil {
return nil, err
}
return s, nil
}
2014-01-24 19:51:56 +00:00
// page retrieves a page reference from the mmap based on the current page size.
2014-01-27 15:11:54 +00:00
func (db *DB) page(id pgid) *page {
return (*page)(unsafe.Pointer(&db.data[id*pgid(db.pageSize)]))
2014-01-24 19:51:56 +00:00
}
// pageInBuffer retrieves a page reference from a given byte array based on the current page size.
2014-01-27 15:11:54 +00:00
func (db *DB) pageInBuffer(b []byte, id pgid) *page {
return (*page)(unsafe.Pointer(&b[id*pgid(db.pageSize)]))
2014-01-12 05:51:01 +00:00
}
2014-01-10 14:32:12 +00:00
2014-01-14 20:01:02 +00:00
// meta retrieves the current meta page reference.
func (db *DB) meta() *meta {
if db.meta0.txnid > db.meta1.txnid {
return db.meta0
}
return db.meta1
}
2014-02-09 22:52:19 +00:00
// allocate returns a contiguous block of memory starting at a given page.
2014-02-11 19:16:12 +00:00
func (db *DB) allocate(count int) (*page, error) {
2014-02-09 22:52:19 +00:00
// Allocate a temporary buffer for the page.
buf := make([]byte, count*db.pageSize)
p := (*page)(unsafe.Pointer(&buf[0]))
p.overflow = uint32(count - 1)
// Use pages from the freelist if they are available.
if p.id = db.freelist.allocate(count); p.id != 0 {
2014-02-11 19:16:12 +00:00
return p, nil
2014-02-09 22:52:19 +00:00
}
2014-02-11 19:16:12 +00:00
// Resize mmap() if we're at the end.
2014-02-09 22:52:19 +00:00
p.id = db.rwtransaction.meta.pgid
2014-02-11 19:16:12 +00:00
var minsz = int((p.id+pgid(count))+1) * db.pageSize
if minsz >= len(db.data) {
if err := db.mmap(minsz); err != nil {
return nil, &Error{"mmap allocate error", err}
}
}
// Move the page id high water mark.
2014-02-09 22:52:19 +00:00
db.rwtransaction.meta.pgid += pgid(count)
2014-02-11 19:16:12 +00:00
return p, nil
2014-02-09 22:52:19 +00:00
}
2014-02-21 16:49:15 +00:00
// Stat represents stats on the database such as free pages and sizes.
type Stat struct {
// PageCount is the total number of allocated pages. This is a high water
// mark in the database that represents how many pages have actually been
// used. This will be smaller than the MmapSize / PageSize.
PageCount int
// FreePageCount is the total number of pages which have been previously
// allocated but are no longer used.
FreePageCount int
// PageSize is the size, in bytes, of individual database pages.
PageSize int
// MmapSize is the mmap-allocated size of the data file. When the data file
// grows beyond this size, the database will obtain a lock on the mmap and
// resize it.
MmapSize int
// TransactionCount is the total number of reader transactions.
TransactionCount int
}