diff --git a/bucket.go b/bucket.go index 6e7b791..d0b2ac2 100644 --- a/bucket.go +++ b/bucket.go @@ -1,5 +1,9 @@ package bolt +const ( + MDB_DUPSORT = 0x04 +) + // TODO: #define MDB_VALID 0x8000 /**< DB handle is valid, for me_dbflags */ // TODO: #define PERSISTENT_FLAGS (0xffff & ~(MDB_VALID)) // TODO: #define VALID_FLAGS (MDB_REVERSEKEY|MDB_DUPSORT|MDB_INTEGERKEY|MDB_DUPFIXED|MDB_INTEGERDUP|MDB_REVERSEDUP|MDB_CREATE) diff --git a/cursor.go b/cursor.go index bd85a3b..e5b3444 100644 --- a/cursor.go +++ b/cursor.go @@ -2,15 +2,18 @@ package bolt // TODO: #define CURSOR_STACK 32 -// TODO: #define C_INITIALIZED 0x01 /**< cursor has been initialized and is valid */ -// TODO: #define C_EOF 0x02 /**< No more data */ -// TODO: #define C_SUB 0x04 /**< Cursor is a sub-cursor */ -// TODO: #define C_DEL 0x08 /**< last op was a cursor_del */ -// TODO: #define C_SPLITTING 0x20 /**< Cursor is in page_split */ -// TODO: #define C_UNTRACK 0x40 /**< Un-track cursor when closing */ +const ( + c_initialized = 0x01 /**< cursor has been initialized and is valid */ + c_eof = 0x02 /**< No more data */ + c_sub = 0x04 /**< Cursor is a sub-cursor */ + c_del = 0x08 /**< last op was a cursor_del */ + c_splitting = 0x20 /**< Cursor is in page_split */ + c_untrack = 0x40 /**< Un-track cursor when closing */ +) // TODO: #define MDB_NOSPILL 0x8000 /** Do not spill pages to disk if txn is getting full, may fail instead */ +/* type Cursor interface { First() error FirstDup() error @@ -28,37 +31,75 @@ type Cursor interface { Set() ([]byte, []byte, error) SetRange() ([]byte, []byte, error) } +*/ -type cursor struct { +type Cursor struct { flags int - _next *cursor - backup *cursor - xcursor *xcursor - transaction *transaction + next *Cursor + backup *Cursor + subcursor *Cursor + transaction *Transaction bucketId int - bucket *Bucket - // bucketx *bucketx - bucketFlag int - snum int - top int - page []*page - ki []int /**< stack of page indices */ + bucket *txnbucket + subbucket *Bucket + // subbucketx *bucketx + subbucketFlag int + snum int + top int + page []*page + ki []int /**< stack of page indices */ } -type xcursor struct { - cursor cursor - bucket *Bucket - // bucketx *bucketx - bucketFlag int +// Initialize a cursor for a given transaction and database. +func (c *Cursor) init(t *Transaction, b *txnbucket, sub *Cursor) { + c.next = nil + c.backup = nil + c.transaction = t + c.bucket = b + c.snum = 0 + c.top = 0 + // TODO: mc->mc_pg[0] = 0; + c.flags = 0 + + if (b.flags & MDB_DUPSORT) != 0 { + sub.subcursor = nil + sub.transaction = t + sub.bucket = b + sub.snum = 0 + sub.top = 0 + sub.flags = c_sub + // TODO: mx->mx_dbx.md_name.mv_size = 0; + // TODO: mx->mx_dbx.md_name.mv_data = NULL; + c.subcursor = sub + } else { + c.subcursor = nil + } + + // Find the root page if the bucket is stale. + if (c.bucket.flags & txnb_stale) != 0 { + c.findPage(nil, ps_rootonly) + } } +// // +// // +// // +// // +// // +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CONVERTED ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // +// // +// // +// // +// // +// // + // Set or clear P_KEEP in dirty, non-overflow, non-sub pages watched by txn. // @param[in] mc A cursor handle for the current operation. // @param[in] pflags Flags of the pages to update: // P_DIRTY to set P_KEEP, P_DIRTY|P_KEEP to clear it. // @param[in] all No shortcuts. Needed except after a full #mdb_page_flush(). // @return 0 on success, non-zero on failure. -func (c *cursor) xkeep(pflags int, all int) error { +func (c *Cursor) xkeep(pflags int, all int) error { /* enum { Mask = P_SUBP|P_DIRTY|P_KEEP }; MDB_txn *txn = mc->mc_txn; @@ -149,7 +190,7 @@ func (c *cursor) xkeep(pflags int, all int) error { // @param[in] key For a put operation, the key being stored. // @param[in] data For a put operation, the data being stored. // @return 0 on success, non-zero on failure. -func (c *cursor) spill(key []byte, data []byte) error { +func (c *Cursor) spill(key []byte, data []byte) error { /* MDB_txn *txn = m0->mc_txn; MDB_page *dp; @@ -424,7 +465,7 @@ func (p *page) copyTo(dst *page, size int) { // Touch a page: make it dirty and re-insert into tree with updated pgno. // @param[in] mc cursor pointing to the page to be touched // @return 0 on success, non-zero on failure. -func (c *cursor) page_touch() int { +func (c *Cursor) page_touch() int { /* MDB_page *mp = mc->mc_pg[mc->mc_top], *np; MDB_txn *txn = mc->mc_txn; @@ -532,7 +573,7 @@ func (c *cursor) page_touch() int { // in *exactp (1 or 0). // Updates the cursor index with the index of the found entry. // If no entry larger or equal to the key is found, returns NULL. -func (c *cursor) search(key []byte) (*node, bool) { +func (c *Cursor) search(key []byte) (*node, bool) { /* unsigned int i = 0, nkeys; int low, high; @@ -623,7 +664,7 @@ func (c *cursor) search(key []byte) (*node, bool) { return nil, false } -func (c *cursor) pop() { +func (c *Cursor) pop() { /* if (mc->mc_snum) { #if MDB_DEBUG @@ -640,7 +681,7 @@ func (c *cursor) pop() { } /** Push a page onto the top of the cursor's stack. */ -func (c *cursor) push(p *page) error { +func (c *Cursor) push(p *page) error { /* DPRINTF(("pushing page %"Z"u on db %d cursor %p", mp->mp_pgno, DDBI(mc), (void *) mc)); @@ -661,7 +702,7 @@ func (c *cursor) push(p *page) error { // Finish #mdb_page_search() / #mdb_page_search_lowest(). // The cursor is at the root page, set up the rest of it. -func (c *cursor) searchRoot(key []byte, flags int) error { +func (c *Cursor) searchRoot(key []byte, flags int) error { /* MDB_page *mp = mc->mc_pg[mc->mc_top]; int rc; @@ -733,7 +774,7 @@ func (c *cursor) searchRoot(key []byte, flags int) error { // before calling mdb_page_search_root(), because the callers // are all in situations where the current page is known to // be underfilled. -func (c *cursor) searchLowest() error { +func (c *Cursor) searchLowest() error { /* MDB_page *mp = mc->mc_pg[mc->mc_top]; MDB_node *node = NODEPTR(mp, 0); @@ -760,7 +801,7 @@ func (c *cursor) searchLowest() error { // This is used by #mdb_cursor_first() and #mdb_cursor_last(). // If MDB_PS_ROOTONLY set, just fetch root node, no further lookups. // @return 0 on success, non-zero on failure. -func (c *cursor) findPage(key []byte, flags int) error { +func (c *Cursor) findPage(key []byte, flags int) error { /* int rc; pgno_t root; @@ -831,7 +872,7 @@ func (c *cursor) findPage(key []byte, flags int) error { return nil } -func (c *cursor) freeOverflowPage(p *page) error { +func (c *Cursor) freeOverflowPage(p *page) error { /* MDB_txn *txn = mc->mc_txn; pgno_t pg = mp->mp_pgno; @@ -913,7 +954,7 @@ func (c *cursor) freeOverflowPage(p *page) error { // @param[in] move_right Non-zero if the right sibling is requested, // otherwise the left sibling. // @return 0 on success, non-zero on failure. -func (c *cursor) sibling(moveRight bool) error { +func (c *Cursor) sibling(moveRight bool) error { /* int rc; MDB_node *indx; @@ -964,7 +1005,7 @@ func (c *cursor) sibling(moveRight bool) error { } // Move the cursor to the next data item. -func (c *cursor) next(key []byte, data []byte, op int) error { +func (c *Cursor) Next(key []byte, data []byte, op int) error { /* MDB_page *mp; MDB_node *leaf; @@ -1046,7 +1087,7 @@ func (c *cursor) next(key []byte, data []byte, op int) error { } // Move the cursor to the previous data item. -func (c *cursor) prev(key []byte, data []byte, op int) error { +func (c *Cursor) prev(key []byte, data []byte, op int) error { /* MDB_page *mp; MDB_node *leaf; @@ -1124,7 +1165,7 @@ func (c *cursor) prev(key []byte, data []byte, op int) error { // Set the cursor on a specific data item. // (bool return is whether it is exact). -func (c *cursor) set(key []byte, data []byte, op int) (error, bool) { +func (c *Cursor) set(key []byte, data []byte, op int) (error, bool) { /* int rc; MDB_page *mp; @@ -1310,7 +1351,7 @@ func (c *cursor) set(key []byte, data []byte, op int) (error, bool) { } // Move the cursor to the first item in the database. -func (c *cursor) first(key []byte, data []byte) error { +func (c *Cursor) first(key []byte, data []byte) error { /* int rc; MDB_node *leaf; @@ -1355,7 +1396,7 @@ func (c *cursor) first(key []byte, data []byte) error { } // Move the cursor to the last item in the database. -func (c *cursor) last() ([]byte, []byte) { +func (c *Cursor) last() ([]byte, []byte) { /* int rc; MDB_node *leaf; @@ -1401,7 +1442,7 @@ func (c *cursor) last() ([]byte, []byte) { return nil, nil } -func (c *cursor) Get(key []byte, data []byte, op int) ([]byte, []byte, error) { +func (c *Cursor) Get(key []byte, data []byte, op int) ([]byte, []byte, error) { /* int rc; int exact = 0; @@ -1569,7 +1610,7 @@ func (c *cursor) Get(key []byte, data []byte, op int) ([]byte, []byte, error) { // Touch all the pages in the cursor stack. Set mc_top. // Makes sure all the pages are writable, before attempting a write operation. // @param[in] mc The cursor to operate on. -func (c *cursor) touch() error { +func (c *Cursor) touch() error { /* int rc = MDB_SUCCESS; @@ -2072,7 +2113,7 @@ func (c *cursor) touch() error { return nil } -func (c *cursor) Del(flags int) error { +func (c *Cursor) Del(flags int) error { /* MDB_node *leaf; MDB_page *mp; @@ -2152,7 +2193,7 @@ func (c *cursor) Del(flags int) error { // unless allocating overflow pages for a large record. // @param[out] mp Address of a page, or NULL on failure. // @return 0 on success, non-zero on failure. -func (c *cursor) newPage(flags int, num int) ([]*page, error) { +func (c *Cursor) newPage(flags int, num int) ([]*page, error) { /* MDB_page *np; int rc; @@ -2194,7 +2235,7 @@ func (c *cursor) newPage(flags int, num int) ([]*page, error) { // should never happen since all callers already calculate the // page's free space before calling this function. // -func (c *cursor) addNode(index int, key []byte, data []byte, pgno int, flags int) error { +func (c *Cursor) addNode(index int, key []byte, data []byte, pgno int, flags int) error { /* unsigned int i; size_t node_size = NODESIZE; @@ -2323,7 +2364,7 @@ func (c *cursor) addNode(index int, key []byte, data []byte, pgno int, flags int // @param[in] indx The index of the node to delete. // @param[in] ksize The size of a node. Only used if the page is // part of a #MDB_DUPFIXED database. -func (c *cursor) deleteNode(ksize int) { +func (c *Cursor) deleteNode(ksize int) { /* MDB_page *mp = mc->mc_pg[mc->mc_top]; indx_t indx = mc->mc_ki[mc->mc_top]; @@ -2375,41 +2416,12 @@ func (c *cursor) deleteNode(ksize int) { */ } -// Initial setup of a sorted-dups cursor. -// Sorted duplicates are implemented as a sub-database for the given key. -// The duplicate data items are actually keys of the sub-database. -// Operations on the duplicate data items are performed using a sub-cursor -// initialized when the sub-database is first accessed. This function does -// the preliminary setup of the sub-cursor, filling in the fields that -// depend only on the parent DB. -// @param[in] mc The main cursor whose sorted-dups cursor is to be initialized. -func (c *cursor) xcursor_init0() { - /* - MDB_xcursor *mx = mc->mc_xcursor; - - mx->mx_cursor.mc_xcursor = NULL; - mx->mx_cursor.mc_txn = mc->mc_txn; - mx->mx_cursor.mc_db = &mx->mx_db; - mx->mx_cursor.mc_dbx = &mx->mx_dbx; - mx->mx_cursor.mc_dbi = mc->mc_dbi; - mx->mx_cursor.mc_dbflag = &mx->mx_dbflag; - mx->mx_cursor.mc_snum = 0; - mx->mx_cursor.mc_top = 0; - mx->mx_cursor.mc_flags = C_SUB; - mx->mx_dbx.md_name.mv_size = 0; - mx->mx_dbx.md_name.mv_data = NULL; - mx->mx_dbx.md_cmp = mc->mc_dbx->md_dcmp; - mx->mx_dbx.md_dcmp = NULL; - mx->mx_dbx.md_rel = mc->mc_dbx->md_rel; - */ -} - // Final setup of a sorted-dups cursor. // Sets up the fields that depend on the data from the main cursor. // @param[in] mc The main cursor whose sorted-dups cursor is to be initialized. // @param[in] node The data containing the #MDB_db record for the // sorted-dup database. -func (c *cursor) xcursor_init1(n *node) { +func (c *Cursor) xcursor_init1(n *node) { /* MDB_xcursor *mx = mc->mc_xcursor; @@ -2455,35 +2467,8 @@ func (c *cursor) xcursor_init1(n *node) { */ } -// Initialize a cursor for a given transaction and database. -func (c *cursor) init(t *transaction, bucket *Bucket, mx *xcursor) { - /* - mc->mc_next = NULL; - mc->mc_backup = NULL; - mc->mc_dbi = dbi; - mc->mc_txn = txn; - mc->mc_db = &txn->mt_dbs[dbi]; - mc->mc_dbx = &txn->mt_dbxs[dbi]; - mc->mc_dbflag = &txn->mt_dbflags[dbi]; - mc->mc_snum = 0; - mc->mc_top = 0; - mc->mc_pg[0] = 0; - mc->mc_flags = 0; - if (txn->mt_dbs[dbi].md_flags & MDB_DUPSORT) { - mdb_tassert(txn, mx != NULL); - mc->mc_xcursor = mx; - mdb_xcursor_init0(mc); - } else { - mc->mc_xcursor = NULL; - } - if (*mc->mc_dbflag & DB_STALE) { - mdb_page_search(mc, NULL, MDB_PS_ROOTONLY); - } - */ -} - // Return the count of duplicate data items for the current key. -func (c *cursor) count() (int, error) { +func (c *Cursor) count() (int, error) { /* MDB_node *leaf; @@ -2507,7 +2492,7 @@ func (c *cursor) count() (int, error) { return 0, nil } -func (c *cursor) Close() { +func (c *Cursor) Close() { /* if (mc && !mc->mc_backup) { // remove from txn, if tracked @@ -2522,23 +2507,19 @@ func (c *cursor) Close() { */ } -func (c *cursor) Transaction() Transaction { - /* - if (!mc) return NULL; - return mc->mc_txn; - */ - return nil +func (c *Cursor) Transaction() *Transaction { + return c.transaction } -func (c *cursor) Bucket() *Bucket { - return c.bucket +func (c *Cursor) Bucket() *Bucket { + return c.bucket.bucket } // Replace the key for a branch node with a new key. // @param[in] mc Cursor pointing to the node to operate on. // @param[in] key The new key to use. // @return 0 on success, non-zero on failure. -func (c *cursor) updateKey(key []byte) error { +func (c *Cursor) updateKey(key []byte) error { /* MDB_page *mp; MDB_node *node; @@ -2609,7 +2590,7 @@ func (c *cursor) updateKey(key []byte) error { } // Move a node from csrc to cdst. -func (c *cursor) moveNodeTo(dst *cursor) error { +func (c *Cursor) moveNodeTo(dst *Cursor) error { /* MDB_node *srcnode; MDB_val key, data; @@ -2786,7 +2767,7 @@ func (c *cursor) moveNodeTo(dst *cursor) error { // the \b csrc page will be freed. // @param[in] csrc Cursor pointing to the source page. // @param[in] cdst Cursor pointing to the destination page. -func (c *cursor) mergePage(dst *cursor) error { +func (c *Cursor) mergePage(dst *Cursor) error { /* int rc; indx_t i, j; @@ -2901,7 +2882,7 @@ func (c *cursor) mergePage(dst *cursor) error { // Copy the contents of a cursor. // @param[in] csrc The cursor to copy from. // @param[out] cdst The cursor to copy to. -func (c *cursor) copyTo(dst *cursor) { +func (c *Cursor) copyTo(dst *Cursor) { /* unsigned int i; @@ -2924,7 +2905,7 @@ func (c *cursor) copyTo(dst *cursor) { // @param[in] mc Cursor pointing to the page where rebalancing // should begin. // @return 0 on success, non-zero on failure. -func (c *cursor) rebalance() error { +func (c *Cursor) rebalance() error { /* MDB_node *node; int rc; @@ -3079,7 +3060,7 @@ func (c *cursor) rebalance() error { } // Complete a delete operation started by #mdb_cursor_del(). -func (c *cursor) del0(leaf *node) error { +func (c *Cursor) del0(leaf *node) error { /* int rc; MDB_page *mp; @@ -3149,7 +3130,7 @@ func (c *cursor) del0(leaf *node) error { // @param[in] newpgno The page number, if the new node is a branch node. // @param[in] nflags The #NODE_ADD_FLAGS for the new node. // @return 0 on success, non-zero on failure. -func (c *cursor) splitPage(newKey []byte, newData []byte, newpgno int, nflags int) error { +func (c *Cursor) splitPage(newKey []byte, newData []byte, newpgno int, nflags int) error { /* unsigned int flags; int rc = MDB_SUCCESS, new_root = 0, did_split = 0; @@ -3533,7 +3514,7 @@ func (c *cursor) splitPage(newKey []byte, newData []byte, newpgno int, nflags in // @param[in] mc Cursor on the DB to free. // @param[in] subs non-Zero to check for sub-DBs in this DB. // @return 0 on success, non-zero on failure. -func (c *cursor) drop0(subs int) error { +func (c *Cursor) drop0(subs int) error { /* int rc; diff --git a/db.go b/db.go index f5c103d..b13ec5f 100644 --- a/db.go +++ b/db.go @@ -15,7 +15,9 @@ const ( IntegerDupKey ) -var DatabaseAlreadyOpenedError = &Error{"Database already open", nil} +var DatabaseNotOpenError = &Error{"db is not open", nil} +var DatabaseAlreadyOpenedError = &Error{"db already open", nil} +var TransactionInProgressError = &Error{"writable transaction is already in progress", nil} // TODO: #define MDB_FATAL_ERROR 0x80000000U /** Failed to update the meta page. Probably an I/O error. */ // TODO: #define MDB_ENV_ACTIVE 0x20000000U /** Some fields are initialized. */ @@ -44,9 +46,9 @@ type DB struct { mmapSize int /**< size of the data memory map */ size int /**< current file size */ pbuf []byte - transaction *transaction /**< current write transaction */ + transaction *Transaction /**< current write transaction */ maxPageNumber int /**< me_mapsize / me_psize */ - pageState pageState /**< state of old pages from freeDB */ + pagestate pagestate /**< state of old pages from freeDB */ dpages []*page /**< list of malloc'd blocks for re-use */ freePages []int /** IDL of pages that became unused in a write txn */ dirtyPages []int /** ID2L of pages written during a write txn. Length MDB_IDL_UM_SIZE. */ @@ -203,12 +205,50 @@ func (db *DB) close() { // TODO } +// Transaction creates a transaction that's associated with this database. +func (db *DB) Transaction(writable bool) (*Transaction, error) { + db.Lock() + defer db.Unlock() + + // Exit if the database is not open yet. + if !db.opened { + return nil, DatabaseNotOpenError + } + // Exit if a writable transaction is currently in progress. + if writable && db.transaction != nil { + return nil, TransactionInProgressError + } + + // Create a transaction associated with the database. + t := &Transaction{ + db: db, + writable: writable, + } + + // We only allow one writable transaction at a time so save the reference. + if writable { + db.transaction = t + } + + return t, nil +} + // page retrieves a page reference from a given byte array based on the current page size. func (db *DB) page(b []byte, id int) *page { return (*page)(unsafe.Pointer(&b[id*db.pageSize])) } +// // +// // +// // +// // +// // // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CONVERTED ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // +// // +// // +// // +// // +// // func (db *DB) freePage(p *page) { /* @@ -266,103 +306,6 @@ func (db *DB) sync(force bool) error { return nil } -func (db *DB) Transaction(parent *transaction, flags int) (*transaction, error) { - /* - MDB_txn *txn; - MDB_ntxn *ntxn; - int rc, size, tsize = sizeof(MDB_txn); - - if (env->me_flags & MDB_FATAL_ERROR) { - DPUTS("environment had fatal error, must shutdown!"); - return MDB_PANIC; - } - if ((env->me_flags & MDB_RDONLY) && !(flags & MDB_RDONLY)) - return EACCES; - if (parent) { - // Nested transactions: Max 1 child, write txns only, no writemap - if (parent->mt_child || - (flags & MDB_RDONLY) || - (parent->mt_flags & (MDB_TXN_RDONLY|MDB_TXN_ERROR)) || - (env->me_flags & MDB_WRITEMAP)) - { - return (parent->mt_flags & MDB_TXN_RDONLY) ? EINVAL : MDB_BAD_TXN; - } - tsize = sizeof(MDB_ntxn); - } - size = tsize + env->me_maxdbs * (sizeof(MDB_db)+1); - if (!(flags & MDB_RDONLY)) - size += env->me_maxdbs * sizeof(MDB_cursor *); - - if ((txn = calloc(1, size)) == NULL) { - DPRINTF(("calloc: %s", strerror(ErrCode()))); - return ENOMEM; - } - txn->mt_dbs = (MDB_db *) ((char *)txn + tsize); - if (flags & MDB_RDONLY) { - txn->mt_flags |= MDB_TXN_RDONLY; - txn->mt_dbflags = (unsigned char *)(txn->mt_dbs + env->me_maxdbs); - } else { - txn->mt_cursors = (MDB_cursor **)(txn->mt_dbs + env->me_maxdbs); - txn->mt_dbflags = (unsigned char *)(txn->mt_cursors + env->me_maxdbs); - } - txn->mt_env = env; - - if (parent) { - unsigned int i; - txn->mt_u.dirty_list = malloc(sizeof(MDB_ID2)*MDB_IDL_UM_SIZE); - if (!txn->mt_u.dirty_list || - !(txn->mt_free_pgs = mdb_midl_alloc(MDB_IDL_UM_MAX))) - { - free(txn->mt_u.dirty_list); - free(txn); - return ENOMEM; - } - txn->mt_txnid = parent->mt_txnid; - txn->mt_dirty_room = parent->mt_dirty_room; - txn->mt_u.dirty_list[0].mid = 0; - txn->mt_spill_pgs = NULL; - txn->mt_next_pgno = parent->mt_next_pgno; - parent->mt_child = txn; - txn->mt_parent = parent; - txn->mt_numdbs = parent->mt_numdbs; - txn->mt_flags = parent->mt_flags; - txn->mt_dbxs = parent->mt_dbxs; - memcpy(txn->mt_dbs, parent->mt_dbs, txn->mt_numdbs * sizeof(MDB_db)); - // Copy parent's mt_dbflags, but clear DB_NEW - for (i=0; imt_numdbs; i++) - txn->mt_dbflags[i] = parent->mt_dbflags[i] & ~DB_NEW; - rc = 0; - ntxn = (MDB_ntxn *)txn; - ntxn->mnt_pgstate = env->me_pgstate; // save parent me_pghead & co - if (env->me_pghead) { - size = MDB_IDL_SIZEOF(env->me_pghead); - env->me_pghead = mdb_midl_alloc(env->me_pghead[0]); - if (env->me_pghead) - memcpy(env->me_pghead, ntxn->mnt_pgstate.mf_pghead, size); - else - rc = ENOMEM; - } - if (!rc) - rc = mdb_cursor_shadow(parent, txn); - if (rc) - mdb_txn_reset0(txn, "beginchild-fail"); - } else { - rc = mdb_txn_renew0(txn); - } - if (rc) - free(txn); - else { - *ret = txn; - DPRINTF(("begin txn %"Z"u%c %p on mdbenv %p, root page %"Z"u", - txn->mt_txnid, (txn->mt_flags & MDB_TXN_RDONLY) ? 'r' : 'w', - (void *) txn, (void *) env, txn->mt_dbs[MAIN_DBI].md_root)); - } - - return rc; - */ - return nil, nil -} - // Check both meta pages to see which one is newer. // @param[in] env the environment handle // @return meta toggle (0 or 1). diff --git a/db_test.go b/db_test.go index cf36c37..b9c4afe 100644 --- a/db_test.go +++ b/db_test.go @@ -180,6 +180,41 @@ func TestDBCorruptMeta1(t *testing.T) { }) } +//-------------------------------------- +// Transaction() +//-------------------------------------- + +// Ensure that a database cannot open a transaction when it's not open. +func TestDBTransactionDatabaseNotOpenError(t *testing.T) { + withDB(func(db *DB, path string) { + txn, err := db.Transaction(false) + assert.Nil(t, txn) + assert.Equal(t, err, DatabaseNotOpenError) + }) +} + +// Ensure that a database cannot open a writable transaction while one is in progress. +func TestDBTransactionInProgressError(t *testing.T) { + withOpenDB(func(db *DB, path string) { + db.Transaction(true) + txn, err := db.Transaction(true) + assert.Nil(t, txn) + assert.Equal(t, err, TransactionInProgressError) + }) +} + +// Ensure that a database can create a new writable transaction. +func TestDBTransactionWriter(t *testing.T) { + withOpenDB(func(db *DB, path string) { + txn, err := db.Transaction(true) + if assert.NotNil(t, txn) { + assert.Equal(t, txn.db, db) + assert.Equal(t, txn.writable, true) + } + assert.NoError(t, err) + }) +} + // withDB executes a function with a database reference. func withDB(fn func(*DB, string)) { f, _ := ioutil.TempFile("", "bolt-") @@ -200,3 +235,14 @@ func withMockDB(fn func(*DB, *mockos, *mocksyscall, string)) { db.syscall = syscall fn(db, os, syscall, "/mock/db") } + +// withOpenDB executes a function with an already opened database. +func withOpenDB(fn func(*DB, string)) { + withDB(func(db *DB, path string) { + if err := db.Open(path, 0666); err != nil { + panic("cannot open db: " + err.Error()) + } + defer db.Close() + fn(db, path) + }) +} diff --git a/page.go b/page.go index 5827f96..33fa0c2 100644 --- a/page.go +++ b/page.go @@ -38,10 +38,12 @@ const maxWriteByteCount uint = 0x80000000 // TODO: #define MAX_WRITE 0x80000000U // #define MDB_COMMIT_PAGES IOV_MAX // #endif -// TODO: #define MDB_PS_MODIFY 1 -// TODO: #define MDB_PS_ROOTONLY 2 -// TODO: #define MDB_PS_FIRST 4 -// TODO: #define MDB_PS_LAST 8 +const ( + MDB_PS_MODIFY = 1 + MDB_PS_ROOTONLY = 2 + MDB_PS_FIRST = 4 + MDB_PS_LAST = 8 +) // TODO: #define MDB_SPLIT_REPLACE MDB_APPENDDUP /**< newkey is not new */ @@ -58,7 +60,7 @@ type page struct { ptr int } -type pageState struct { +type pagestate struct { head int /**< Reclaimed freeDB pages, or NULL before use */ last int /**< ID of last used record, or 0 if !mf_pghead */ } diff --git a/transaction.go b/transaction.go index 4ca2d35..4525a31 100644 --- a/transaction.go +++ b/transaction.go @@ -1,44 +1,143 @@ package bolt -// TODO: #define DB_DIRTY 0x01 /**< DB was modified or is DUPSORT data */ -// TODO: #define DB_STALE 0x02 /**< Named-DB record is older than txnID */ -// TODO: #define DB_NEW 0x04 /**< Named-DB handle opened in this txn */ -// TODO: #define DB_VALID 0x08 /**< DB handle is valid, see also #MDB_VALID */ +var TransactionExistingChildError = &Error{"txn already has a child", nil} +var TransactionReadOnlyChildError = &Error{"read-only txn cannot create a child", nil} -// TODO: #define MDB_TXN_RDONLY 0x01 /**< read-only transaction */ -// TODO: #define MDB_TXN_ERROR 0x02 /**< an error has occurred */ -// TODO: #define MDB_TXN_DIRTY 0x04 /**< must write, even if dirty list is empty */ -// TODO: #define MDB_TXN_SPILLS 0x08 /**< txn or a parent has spilled pages */ +const ( + txnb_dirty = 0x01 /**< DB was modified or is DUPSORT data */ + txnb_stale = 0x02 /**< Named-DB record is older than txnID */ + txnb_new = 0x04 /**< Named-DB handle opened in this txn */ + txnb_valid = 0x08 /**< DB handle is valid, see also #MDB_VALID */ +) -type Transaction interface { -} +const ( + ps_modify = 1 + ps_rootonly = 2 + ps_first = 4 + ps_last = 8 +) -type transaction struct { - id int - flags int - db *DB - parent *transaction - child *transaction - nextPageNumber int - freePages []int - spillPages []int - dirtyList []int - reader *reader +type Transaction struct { + id int + db *DB + writable bool + dirty bool + spilled bool + err error + parent *Transaction + child *Transaction + buckets []*txnbucket + + pgno int + freePages []pgno + spillPages []pgno + dirtyList []pgno + reader *reader // TODO: bucketxs []*bucketx - buckets []*Bucket - bucketFlags []int - cursors []*cursor // Implicit from slices? TODO: MDB_dbi mt_numdbs; - mt_dirty_room int + dirty_room int + pagestate pagestate } -// ntxn represents a nested transaction. -type ntxn struct { - transaction *transaction /**< the transaction */ - pageState pageState /**< parent transaction's saved freestate */ +type txnbucket struct { + bucket *Bucket + cursor *Cursor + flags int } -func (t *transaction) allocPage(num int) *page { +// Transaction begins a nested child transaction. Child transactions are only +// available to writable transactions and a transaction can only have one child +// at a time. +func (t *Transaction) Transaction() (*Transaction, error) { + // Exit if parent already has a child transaction. + if t.child != nil { + return nil, TransactionExistingChildError + } + // Exit if using parent for read-only transaction. + if !t.writable { + return nil, TransactionReadOnlyChildError + } + // TODO: Exit if parent is in an error state. + + // Create the child transaction and attach the parent. + child := &Transaction{ + id: t.id, + db: t.db, + parent: t, + writable: true, + pgno: t.pgno, + pagestate: t.db.pagestate, + } + copy(child.buckets, t.buckets) + + // TODO: Remove DB_NEW flag. + t.child = child + + // TODO: wtf? + // if (env->me_pghead) { + // size = MDB_IDL_SIZEOF(env->me_pghead); + // env->me_pghead = mdb_midl_alloc(env->me_pghead[0]); + // if (env->me_pghead) + // memcpy(env->me_pghead, ntxn->mnt_pgstate.mf_pghead, size); + // else + // rc = ENOMEM; + // } + + // TODO: Back up parent transaction's cursors. + // if t.shadow(child); err != nil { + // child.reset0() + // return err + // } + + return child, nil +} + +func (t *Transaction) Cursor(b *Bucket) (*Cursor, error) { + // TODO: if !(txn->mt_dbflags[dbi] & DB_VALID) return InvalidBucketError + // TODO: if (txn->mt_flags & MDB_TXN_ERROR) return BadTransactionError + + // Allow read access to the freelist + // TODO: if (!dbi && !F_ISSET(txn->mt_flags, MDB_TXN_RDONLY)) + + /* + MDB_cursor *mc; + size_t size = sizeof(MDB_cursor); + + // Allow read access to the freelist + if (!dbi && !F_ISSET(txn->mt_flags, MDB_TXN_RDONLY)) + return EINVAL; + + if ((mc = malloc(size)) != NULL) { + mdb_cursor_init(mc, txn, dbi, (MDB_xcursor *)(mc + 1)); + if (txn->mt_cursors) { + mc->mc_next = txn->mt_cursors[dbi]; + txn->mt_cursors[dbi] = mc; + mc->mc_flags |= C_UNTRACK; + } + } else { + return ENOMEM; + } + + *ret = mc; + + return MDB_SUCCESS; + */ + return nil, nil +} + +// // +// // +// // +// // +// // +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CONVERTED ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // +// // +// // +// // +// // +// // + +func (t *Transaction) allocPage(num int) *page { /* MDB_env *env = txn->mt_env; MDB_page *ret = env->me_dpages; @@ -74,7 +173,7 @@ func (t *transaction) allocPage(num int) *page { } // Find oldest txnid still referenced. Expects txn->mt_txnid > 0. -func (t *transaction) oldest() int { +func (t *Transaction) oldest() int { /* int i; txnid_t mr, oldest = txn->mt_txnid - 1; @@ -94,7 +193,7 @@ func (t *transaction) oldest() int { } // Add a page to the txn's dirty list -func (t *transaction) dirty(p *page) { +func (t *Transaction) addDirtyPage(p *page) { /* MDB_ID2 mid; int rc, (*insert)(MDB_ID2L, MDB_ID2 *); @@ -119,7 +218,7 @@ func (t *transaction) dirty(p *page) { // @param[in] mp the page being referenced. It must not be dirty. // @param[out] ret the writable page, if any. ret is unchanged if // mp wasn't spilled. -func (t *transaction) unspill(p *page) *page { +func (t *Transaction) unspill(p *page) *page { /* MDB_env *env = txn->mt_env; const MDB_txn *tx2; @@ -173,7 +272,7 @@ func (t *transaction) unspill(p *page) *page { } // Back up parent txn's cursors, then grab the originals for tracking -func (t *transaction) shadow(dst *transaction) error { +func (t *Transaction) shadow(dst *Transaction) error { /* MDB_cursor *mc, *bk; MDB_xcursor *mx; @@ -214,7 +313,7 @@ func (t *transaction) shadow(dst *transaction) error { // @param[in] txn the transaction handle. // @param[in] merge true to keep changes to parent cursors, false to revert. // @return 0 on success, non-zero on failure. -func (t *transaction) closeCursors(merge bool) { +func (t *Transaction) closeCursors(merge bool) { /* MDB_cursor **cursors = txn->mt_cursors, *mc, *next, *bk; MDB_xcursor *mx; @@ -252,7 +351,7 @@ func (t *transaction) closeCursors(merge bool) { // Common code for #mdb_txn_begin() and #mdb_txn_renew(). // @param[in] txn the transaction handle to initialize // @return 0 on success, non-zero on failure. -func (t *transaction) renew() error { +func (t *Transaction) renew() error { /* MDB_env *env = txn->mt_env; MDB_txninfo *ti = env->me_txns; @@ -366,7 +465,7 @@ func (t *transaction) renew() error { return nil } -func (t *transaction) Renew() error { +func (t *Transaction) Renew() error { /* int rc; @@ -389,12 +488,12 @@ func (t *transaction) Renew() error { return nil } -func (t *transaction) DB() *DB { +func (t *Transaction) DB() *DB { return t.db } // Export or close DBI handles opened in this txn. -func (t *transaction) updateBuckets(keep bool) { +func (t *Transaction) updateBuckets(keep bool) { /* int i; MDB_dbi n = txn->mt_numdbs; @@ -423,7 +522,7 @@ func (t *transaction) updateBuckets(keep bool) { // May be called twice for readonly txns: First reset it, then abort. // @param[in] txn the transaction handle to reset // @param[in] act why the transaction is being reset -func (t *transaction) reset(act string) { +func (t *Transaction) reset(act string) { /* MDB_env *env = txn->mt_env; @@ -472,7 +571,7 @@ func (t *transaction) reset(act string) { */ } -func (t *transaction) Reset() { +func (t *Transaction) Reset() { /* if (txn == NULL) return; @@ -485,7 +584,7 @@ func (t *transaction) Reset() { */ } -func (t *transaction) Abort() { +func (t *Transaction) Abort() { /* if (txn == NULL) return; @@ -504,7 +603,7 @@ func (t *transaction) Abort() { // Save the freelist as of this transaction to the freeDB. // This changes the freelist. Keep trying until it stabilizes. -func (t *transaction) saveFreelist() error { +func (t *Transaction) saveFreelist() error { /* // env->me_pghead[] can grow and shrink during this call. // env->me_pglast and txn->mt_free_pgs[] can only grow. @@ -662,7 +761,7 @@ func (t *transaction) saveFreelist() error { // @param[in] txn the transaction that's being committed // @param[in] keep number of initial pages in dirty_list to keep dirty. // @return 0 on success, non-zero on failure. -func (t *transaction) flush(keep bool) error { +func (t *Transaction) flush(keep bool) error { /* MDB_env *env = txn->mt_env; MDB_ID2L dl = txn->mt_u.dirty_list; @@ -1005,7 +1104,7 @@ func (t *transaction) flush(keep bool) error { // Update the environment info to commit a transaction. // @param[in] txn the transaction that's being committed // @return 0 on success, non-zero on failure. -func (t *transaction) writeMeta() error { +func (t *Transaction) writeMeta() error { /* MDB_env *env; MDB_meta meta, metab, *mp; @@ -1129,7 +1228,7 @@ func (t *transaction) writeMeta() error { // @param[out] ret address of a pointer where the page's address will be stored. // @param[out] lvl dirty_list inheritance level of found page. 1=current txn, 0=mapped page. // @return 0 on success, non-zero on failure. -func (t *transaction) getPage(id int) (*page, int, error) { +func (t *Transaction) getPage(id int) (*page, int, error) { /* MDB_env *env = txn->mt_env; MDB_page *p = NULL; @@ -1188,7 +1287,7 @@ func (t *transaction) getPage(id int) (*page, int, error) { // @param[in] leaf The node being read. // @param[out] data Updated to point to the node's data. // @return 0 on success, non-zero on failure. -func (t *transaction) readNode(leaf *node, data []byte) error { +func (t *Transaction) readNode(leaf *node, data []byte) error { /* MDB_page *omp; // overflow page pgno_t pgno; @@ -1214,7 +1313,7 @@ func (t *transaction) readNode(leaf *node, data []byte) error { return nil } -func (t *transaction) Get(bucket Bucket, key []byte) ([]byte, error) { +func (t *Transaction) Get(bucket Bucket, key []byte) ([]byte, error) { /* MDB_cursor mc; MDB_xcursor mx; @@ -1238,43 +1337,7 @@ func (t *transaction) Get(bucket Bucket, key []byte) ([]byte, error) { return nil, nil } -func (t *transaction) Cursor(b Bucket) (Cursor, error) { - /* - MDB_cursor *mc; - size_t size = sizeof(MDB_cursor); - - if (txn == NULL || ret == NULL || dbi >= txn->mt_numdbs || !(txn->mt_dbflags[dbi] & DB_VALID)) - return EINVAL; - - if (txn->mt_flags & MDB_TXN_ERROR) - return MDB_BAD_TXN; - - // Allow read access to the freelist - if (!dbi && !F_ISSET(txn->mt_flags, MDB_TXN_RDONLY)) - return EINVAL; - - if (txn->mt_dbs[dbi].md_flags & MDB_DUPSORT) - size += sizeof(MDB_xcursor); - - if ((mc = malloc(size)) != NULL) { - mdb_cursor_init(mc, txn, dbi, (MDB_xcursor *)(mc + 1)); - if (txn->mt_cursors) { - mc->mc_next = txn->mt_cursors[dbi]; - txn->mt_cursors[dbi] = mc; - mc->mc_flags |= C_UNTRACK; - } - } else { - return ENOMEM; - } - - *ret = mc; - - return MDB_SUCCESS; - */ - return nil, nil -} - -func (t *transaction) Renew1(c Cursor) error { +func (t *Transaction) Renew1(c Cursor) error { /* if (txn == NULL || mc == NULL || mc->mc_dbi >= txn->mt_numdbs) return EINVAL; @@ -1288,7 +1351,7 @@ func (t *transaction) Renew1(c Cursor) error { return nil } -func (t *transaction) Delete(b *Bucket, key []byte, data []byte) error { +func (t *Transaction) Delete(b *Bucket, key []byte, data []byte) error { /* MDB_cursor mc; MDB_xcursor mx; @@ -1343,7 +1406,7 @@ func (t *transaction) Delete(b *Bucket, key []byte, data []byte) error { return nil } -func (t *transaction) Put(b Bucket, key []byte, data []byte, flags int) error { +func (t *Transaction) Put(b Bucket, key []byte, data []byte, flags int) error { /* MDB_cursor mc; MDB_xcursor mx; @@ -1363,7 +1426,7 @@ func (t *transaction) Put(b Bucket, key []byte, data []byte, flags int) error { return nil } -func (t *transaction) Bucket(name string, flags int) (*Bucket, error) { +func (t *Transaction) Bucket(name string, flags int) (*Bucket, error) { /* MDB_val key, data; MDB_dbi i; @@ -1467,7 +1530,7 @@ func (t *transaction) Bucket(name string, flags int) (*Bucket, error) { return nil, nil } -func (t *transaction) Stat(b Bucket) *stat { +func (t *Transaction) Stat(b Bucket) *stat { /* if (txn == NULL || arg == NULL || dbi >= txn->mt_numdbs) return EINVAL; @@ -1483,7 +1546,7 @@ func (t *transaction) Stat(b Bucket) *stat { return nil } -func (t *transaction) BucketFlags(b Bucket) (int, error) { +func (t *Transaction) BucketFlags(b Bucket) (int, error) { /* // We could return the flags for the FREE_DBI too but what's the point? if (txn == NULL || dbi < MAIN_DBI || dbi >= txn->mt_numdbs) @@ -1494,7 +1557,7 @@ func (t *transaction) BucketFlags(b Bucket) (int, error) { return 0, nil } -func (t *transaction) Drop(b *Bucket, del int) error { +func (t *Transaction) Drop(b *Bucket, del int) error { /* MDB_cursor *mc, *m2; int rc;