Skip to content

Commit 019c34e

Browse files
committed
add method Inspect to inspect bucket structure
Also added a related command: bbolt inspect db The outputed etcd data structure: { "name": "root", "keyN": 0, "children": [ { "name": "alarm", "keyN": 0 }, { "name": "auth", "keyN": 2 }, { "name": "authRoles", "keyN": 1 }, { "name": "authUsers", "keyN": 1 }, { "name": "cluster", "keyN": 1 }, { "name": "key", "keyN": 1285 }, { "name": "lease", "keyN": 2 }, { "name": "members", "keyN": 1 }, { "name": "members_removed", "keyN": 0 }, { "name": "meta", "keyN": 3 } ] } Signed-off-by: Benjamin Wang <[email protected]>
1 parent 4a059b4 commit 019c34e

10 files changed

+295
-10
lines changed

bucket.go

+30
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,30 @@ func (b *Bucket) MoveBucket(key []byte, dstBucket *Bucket) (err error) {
392392
return nil
393393
}
394394

395+
// Inspect returns the structure of the bucket.
396+
func (b *Bucket) Inspect() BucketStructure {
397+
return b.recursivelyInspect([]byte("root"))
398+
}
399+
400+
func (b *Bucket) recursivelyInspect(name []byte) BucketStructure {
401+
bs := BucketStructure{Name: string(name)}
402+
403+
keyN := 0
404+
c := b.Cursor()
405+
for k, _, flags := c.first(); k != nil; k, _, flags = c.next() {
406+
if flags&common.BucketLeafFlag != 0 {
407+
childBucket := b.Bucket(k)
408+
childBS := childBucket.recursivelyInspect(k)
409+
bs.Children = append(bs.Children, childBS)
410+
} else {
411+
keyN++
412+
}
413+
}
414+
bs.KeyN = keyN
415+
416+
return bs
417+
}
418+
395419
// Get retrieves the value for a key in the bucket.
396420
// Returns a nil value if the key does not exist or if the key is a nested bucket.
397421
// The returned value is only valid for the life of the transaction.
@@ -955,3 +979,9 @@ func cloneBytes(v []byte) []byte {
955979
copy(clone, v)
956980
return clone
957981
}
982+
983+
type BucketStructure struct {
984+
Name string `json:"name"` // name of the bucket
985+
KeyN int `json:"keyN"` // number of key/value pairs
986+
Children []BucketStructure `json:"buckets,omitempty"` // child buckets
987+
}

bucket_test.go

+105
Original file line numberDiff line numberDiff line change
@@ -1623,6 +1623,111 @@ func TestBucket_Stats_Nested(t *testing.T) {
16231623
}
16241624
}
16251625

1626+
func TestBucket_Inspect(t *testing.T) {
1627+
db := btesting.MustCreateDB(t)
1628+
1629+
expectedStructure := bolt.BucketStructure{
1630+
Name: "root",
1631+
KeyN: 0,
1632+
Children: []bolt.BucketStructure{
1633+
{
1634+
Name: "b1",
1635+
KeyN: 3,
1636+
Children: []bolt.BucketStructure{
1637+
{
1638+
Name: "b1_1",
1639+
KeyN: 6,
1640+
},
1641+
{
1642+
Name: "b1_2",
1643+
KeyN: 7,
1644+
},
1645+
{
1646+
Name: "b1_3",
1647+
KeyN: 8,
1648+
},
1649+
},
1650+
},
1651+
{
1652+
Name: "b2",
1653+
KeyN: 4,
1654+
Children: []bolt.BucketStructure{
1655+
{
1656+
Name: "b2_1",
1657+
KeyN: 10,
1658+
},
1659+
{
1660+
Name: "b2_2",
1661+
KeyN: 12,
1662+
Children: []bolt.BucketStructure{
1663+
{
1664+
Name: "b2_2_1",
1665+
KeyN: 2,
1666+
},
1667+
{
1668+
Name: "b2_2_2",
1669+
KeyN: 3,
1670+
},
1671+
},
1672+
},
1673+
{
1674+
Name: "b2_3",
1675+
KeyN: 11,
1676+
},
1677+
},
1678+
},
1679+
},
1680+
}
1681+
1682+
type bucketItem struct {
1683+
b *bolt.Bucket
1684+
bs bolt.BucketStructure
1685+
}
1686+
1687+
t.Log("Populating the database")
1688+
err := db.Update(func(tx *bolt.Tx) error {
1689+
queue := []bucketItem{
1690+
{
1691+
b: nil,
1692+
bs: expectedStructure,
1693+
},
1694+
}
1695+
1696+
for len(queue) > 0 {
1697+
item := queue[0]
1698+
queue = queue[1:]
1699+
1700+
if item.b != nil {
1701+
for i := 0; i < item.bs.KeyN; i++ {
1702+
err := item.b.Put([]byte(fmt.Sprintf("%02d", i)), []byte(fmt.Sprintf("%02d", i)))
1703+
require.NoError(t, err)
1704+
}
1705+
1706+
for _, child := range item.bs.Children {
1707+
childBucket, err := item.b.CreateBucket([]byte(child.Name))
1708+
require.NoError(t, err)
1709+
queue = append(queue, bucketItem{b: childBucket, bs: child})
1710+
}
1711+
} else {
1712+
for _, child := range item.bs.Children {
1713+
childBucket, err := tx.CreateBucket([]byte(child.Name))
1714+
require.NoError(t, err)
1715+
queue = append(queue, bucketItem{b: childBucket, bs: child})
1716+
}
1717+
}
1718+
}
1719+
return nil
1720+
})
1721+
require.NoError(t, err)
1722+
1723+
t.Log("Inspecting the database")
1724+
_ = db.View(func(tx *bolt.Tx) error {
1725+
actualStructure := tx.Inspect()
1726+
assert.Equal(t, expectedStructure, actualStructure)
1727+
return nil
1728+
})
1729+
}
1730+
16261731
// Ensure a large bucket can calculate stats.
16271732
func TestBucket_Stats_Large(t *testing.T) {
16281733
if testing.Short() {

cmd/bbolt/README.md

+55
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,61 @@
162162
Bytes used for inlined buckets: 780 (0%)
163163
```
164164

165+
### inspect
166+
- `inspect` inspect the structure of the database.
167+
- Usage: `bbolt inspect [path to the bbolt database]`
168+
169+
Example:
170+
```bash
171+
$ ./bbolt inspect ~/default.etcd/member/snap/db
172+
{
173+
"name": "root",
174+
"keyN": 0,
175+
"buckets": [
176+
{
177+
"name": "alarm",
178+
"keyN": 0
179+
},
180+
{
181+
"name": "auth",
182+
"keyN": 2
183+
},
184+
{
185+
"name": "authRoles",
186+
"keyN": 1
187+
},
188+
{
189+
"name": "authUsers",
190+
"keyN": 1
191+
},
192+
{
193+
"name": "cluster",
194+
"keyN": 1
195+
},
196+
{
197+
"name": "key",
198+
"keyN": 1285
199+
},
200+
{
201+
"name": "lease",
202+
"keyN": 2
203+
},
204+
{
205+
"name": "members",
206+
"keyN": 1
207+
},
208+
{
209+
"name": "members_removed",
210+
"keyN": 0
211+
},
212+
{
213+
"name": "meta",
214+
"keyN": 3
215+
}
216+
]
217+
}
218+
```
219+
165220
### pages
166221

167222
- Pages prints a table of pages with their type (meta, leaf, branch, freelist).

cmd/bbolt/command_inspect.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"os"
8+
9+
"github.com/spf13/cobra"
10+
11+
bolt "go.etcd.io/bbolt"
12+
)
13+
14+
func newInspectCobraCommand() *cobra.Command {
15+
inspectCmd := &cobra.Command{
16+
Use: "inspect",
17+
Short: "inspect the structure of the database",
18+
Args: func(cmd *cobra.Command, args []string) error {
19+
if len(args) == 0 {
20+
return errors.New("db file path not provided")
21+
}
22+
if len(args) > 1 {
23+
return errors.New("too many arguments")
24+
}
25+
return nil
26+
},
27+
RunE: func(cmd *cobra.Command, args []string) error {
28+
return inspectFunc(args[0])
29+
},
30+
}
31+
32+
return inspectCmd
33+
}
34+
35+
func inspectFunc(srcDBPath string) error {
36+
if _, err := checkSourceDBPath(srcDBPath); err != nil {
37+
return err
38+
}
39+
40+
db, err := bolt.Open(srcDBPath, 0600, &bolt.Options{ReadOnly: true})
41+
if err != nil {
42+
return err
43+
}
44+
defer db.Close()
45+
46+
return db.View(func(tx *bolt.Tx) error {
47+
bs := tx.Inspect()
48+
out, err := json.MarshalIndent(bs, "", " ")
49+
if err != nil {
50+
return err
51+
}
52+
fmt.Fprintln(os.Stdout, string(out))
53+
return nil
54+
})
55+
}

cmd/bbolt/command_inspect_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package main_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
bolt "go.etcd.io/bbolt"
9+
main "go.etcd.io/bbolt/cmd/bbolt"
10+
"go.etcd.io/bbolt/internal/btesting"
11+
)
12+
13+
func TestInspect(t *testing.T) {
14+
pageSize := 4096
15+
db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: pageSize})
16+
srcPath := db.Path()
17+
db.Close()
18+
19+
defer requireDBNoChange(t, dbData(t, db.Path()), db.Path())
20+
21+
rootCmd := main.NewRootCommand()
22+
rootCmd.SetArgs([]string{
23+
"inspect", srcPath,
24+
})
25+
err := rootCmd.Execute()
26+
require.NoError(t, err)
27+
}

cmd/bbolt/command_root.go

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func NewRootCommand() *cobra.Command {
1919
rootCmd.AddCommand(
2020
newVersionCobraCommand(),
2121
newSurgeryCobraCommand(),
22+
newInspectCobraCommand(),
2223
)
2324

2425
return rootCmd

cmd/bbolt/command_surgery.go

-10
Original file line numberDiff line numberDiff line change
@@ -330,13 +330,3 @@ func readMetaPage(path string) (*common.Meta, error) {
330330
}
331331
return m[1], nil
332332
}
333-
334-
func checkSourceDBPath(srcPath string) (os.FileInfo, error) {
335-
fi, err := os.Stat(srcPath)
336-
if os.IsNotExist(err) {
337-
return nil, fmt.Errorf("source database file %q doesn't exist", srcPath)
338-
} else if err != nil {
339-
return nil, fmt.Errorf("failed to open source database file %q: %v", srcPath, err)
340-
}
341-
return fi, nil
342-
}

cmd/bbolt/main.go

+1
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ The commands are:
170170
pages print list of pages with their types
171171
page-item print the key and value of a page item.
172172
stats iterate over all pages and generate usage stats
173+
inspect inspect the structure of the database
173174
surgery perform surgery on bbolt database
174175
175176
Use "bbolt [command] -h" for more information about a command.

cmd/bbolt/utils.go

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
)
7+
8+
func checkSourceDBPath(srcPath string) (os.FileInfo, error) {
9+
fi, err := os.Stat(srcPath)
10+
if os.IsNotExist(err) {
11+
return nil, fmt.Errorf("source database file %q doesn't exist", srcPath)
12+
} else if err != nil {
13+
return nil, fmt.Errorf("failed to open source database file %q: %v", srcPath, err)
14+
}
15+
return fi, nil
16+
}

tx.go

+5
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ func (tx *Tx) Stats() TxStats {
100100
return tx.stats
101101
}
102102

103+
// Inspect returns the structure of the database.
104+
func (tx *Tx) Inspect() BucketStructure {
105+
return tx.root.Inspect()
106+
}
107+
103108
// Bucket retrieves a bucket by name.
104109
// Returns nil if the bucket does not exist.
105110
// The bucket instance is only valid for the lifetime of the transaction.

0 commit comments

Comments
 (0)