Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Inspect method & command to inspect db structure #674

Merged
merged 1 commit into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,30 @@ func (b *Bucket) MoveBucket(key []byte, dstBucket *Bucket) (err error) {
return nil
}

// Inspect returns the structure of the bucket.
func (b *Bucket) Inspect() BucketStructure {
return b.recursivelyInspect([]byte("root"))
}

func (b *Bucket) recursivelyInspect(name []byte) BucketStructure {
bs := BucketStructure{Name: string(name)}

keyN := 0
c := b.Cursor()
for k, _, flags := c.first(); k != nil; k, _, flags = c.next() {
if flags&common.BucketLeafFlag != 0 {
childBucket := b.Bucket(k)
childBS := childBucket.recursivelyInspect(k)
bs.Children = append(bs.Children, childBS)
} else {
keyN++
}
}
bs.KeyN = keyN

return bs
}

// Get retrieves the value for a key in the bucket.
// Returns a nil value if the key does not exist or if the key is a nested bucket.
// The returned value is only valid for the life of the transaction.
Expand Down Expand Up @@ -955,3 +979,9 @@ func cloneBytes(v []byte) []byte {
copy(clone, v)
return clone
}

type BucketStructure struct {
Name string `json:"name"` // name of the bucket
KeyN int `json:"keyN"` // number of key/value pairs
Children []BucketStructure `json:"buckets,omitempty"` // child buckets
}
105 changes: 105 additions & 0 deletions bucket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1623,6 +1623,111 @@ func TestBucket_Stats_Nested(t *testing.T) {
}
}

func TestBucket_Inspect(t *testing.T) {
db := btesting.MustCreateDB(t)

expectedStructure := bolt.BucketStructure{
Name: "root",
KeyN: 0,
Children: []bolt.BucketStructure{
{
Name: "b1",
KeyN: 3,
Children: []bolt.BucketStructure{
{
Name: "b1_1",
KeyN: 6,
},
{
Name: "b1_2",
KeyN: 7,
},
{
Name: "b1_3",
KeyN: 8,
},
},
},
{
Name: "b2",
KeyN: 4,
Children: []bolt.BucketStructure{
{
Name: "b2_1",
KeyN: 10,
},
{
Name: "b2_2",
KeyN: 12,
Children: []bolt.BucketStructure{
{
Name: "b2_2_1",
KeyN: 2,
},
{
Name: "b2_2_2",
KeyN: 3,
},
},
},
{
Name: "b2_3",
KeyN: 11,
},
},
},
},
}

type bucketItem struct {
b *bolt.Bucket
bs bolt.BucketStructure
}

t.Log("Populating the database")
err := db.Update(func(tx *bolt.Tx) error {
queue := []bucketItem{
{
b: nil,
bs: expectedStructure,
},
}

for len(queue) > 0 {
item := queue[0]
queue = queue[1:]

if item.b != nil {
for i := 0; i < item.bs.KeyN; i++ {
err := item.b.Put([]byte(fmt.Sprintf("%02d", i)), []byte(fmt.Sprintf("%02d", i)))
require.NoError(t, err)
}

for _, child := range item.bs.Children {
childBucket, err := item.b.CreateBucket([]byte(child.Name))
require.NoError(t, err)
queue = append(queue, bucketItem{b: childBucket, bs: child})
}
} else {
for _, child := range item.bs.Children {
childBucket, err := tx.CreateBucket([]byte(child.Name))
require.NoError(t, err)
queue = append(queue, bucketItem{b: childBucket, bs: child})
}
}
}
return nil
})
require.NoError(t, err)

t.Log("Inspecting the database")
_ = db.View(func(tx *bolt.Tx) error {
actualStructure := tx.Inspect()
assert.Equal(t, expectedStructure, actualStructure)
return nil
})
}

// Ensure a large bucket can calculate stats.
func TestBucket_Stats_Large(t *testing.T) {
if testing.Short() {
Expand Down
55 changes: 55 additions & 0 deletions cmd/bbolt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,61 @@
Bytes used for inlined buckets: 780 (0%)
```

### inspect
- `inspect` inspect the structure of the database.
- Usage: `bbolt inspect [path to the bbolt database]`

Example:
```bash
$ ./bbolt inspect ~/default.etcd/member/snap/db
{
"name": "root",
"keyN": 0,
"buckets": [
{
"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
}
]
}
```

### pages

- Pages prints a table of pages with their type (meta, leaf, branch, freelist).
Expand Down
55 changes: 55 additions & 0 deletions cmd/bbolt/command_inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"encoding/json"
"errors"
"fmt"
"os"

"github.com/spf13/cobra"

bolt "go.etcd.io/bbolt"
)

func newInspectCobraCommand() *cobra.Command {
inspectCmd := &cobra.Command{
Use: "inspect",
Short: "inspect the structure of the database",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("db file path not provided")
}
if len(args) > 1 {
return errors.New("too many arguments")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return inspectFunc(args[0])
},
}

return inspectCmd
}

func inspectFunc(srcDBPath string) error {
if _, err := checkSourceDBPath(srcDBPath); err != nil {
return err
}

db, err := bolt.Open(srcDBPath, 0600, &bolt.Options{ReadOnly: true})
if err != nil {
return err
}
defer db.Close()

return db.View(func(tx *bolt.Tx) error {
bs := tx.Inspect()
out, err := json.MarshalIndent(bs, "", " ")
if err != nil {
return err
}
fmt.Fprintln(os.Stdout, string(out))
return nil
})
}
27 changes: 27 additions & 0 deletions cmd/bbolt/command_inspect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main_test

import (
"testing"

"github.com/stretchr/testify/require"

bolt "go.etcd.io/bbolt"
main "go.etcd.io/bbolt/cmd/bbolt"
"go.etcd.io/bbolt/internal/btesting"
)

func TestInspect(t *testing.T) {
pageSize := 4096
db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: pageSize})
srcPath := db.Path()
db.Close()

defer requireDBNoChange(t, dbData(t, db.Path()), db.Path())

rootCmd := main.NewRootCommand()
rootCmd.SetArgs([]string{
"inspect", srcPath,
})
err := rootCmd.Execute()
require.NoError(t, err)
}
1 change: 1 addition & 0 deletions cmd/bbolt/command_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func NewRootCommand() *cobra.Command {
rootCmd.AddCommand(
newVersionCobraCommand(),
newSurgeryCobraCommand(),
newInspectCobraCommand(),
)

return rootCmd
Expand Down
10 changes: 0 additions & 10 deletions cmd/bbolt/command_surgery.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,13 +330,3 @@ func readMetaPage(path string) (*common.Meta, error) {
}
return m[1], nil
}

func checkSourceDBPath(srcPath string) (os.FileInfo, error) {
fi, err := os.Stat(srcPath)
if os.IsNotExist(err) {
return nil, fmt.Errorf("source database file %q doesn't exist", srcPath)
} else if err != nil {
return nil, fmt.Errorf("failed to open source database file %q: %v", srcPath, err)
}
return fi, nil
}
1 change: 1 addition & 0 deletions cmd/bbolt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ The commands are:
pages print list of pages with their types
page-item print the key and value of a page item.
stats iterate over all pages and generate usage stats
inspect inspect the structure of the database

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious: was there a reason not to add this to the Run method as well?

Currently it seems the inspect command does not work, but the code is there thanks to this commit. trying to understand if that's intentional

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the commands are not found in the Run method, bbolt will automatically try to run the cobra commands,

bbolt/cmd/bbolt/main.go

Lines 68 to 69 in 92c7414

} else if err == ErrUnknownCommand {
cobraExecute()

Currently it seems the inspect command does not work

Have you tried it out? This isn't the first time I see this false statement. Please try it out, let me know whether you really have issue.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using 1.3.11 I get:

$ bbolt inspect /tmp/meta.db
unknown command

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok it seems the latest released version v1.3.11 does not have the changes you referenced (

func main() {
). It was confusing though because the README references this command. I suspect that's why others are tripping up as well

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New features, including this one, only go into main branch.

So please try the main branch or v1.4.0-beta.0

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

surgery perform surgery on bbolt database

Use "bbolt [command] -h" for more information about a command.
Expand Down
16 changes: 16 additions & 0 deletions cmd/bbolt/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package main

import (
"fmt"
"os"
)

func checkSourceDBPath(srcPath string) (os.FileInfo, error) {
fi, err := os.Stat(srcPath)
if os.IsNotExist(err) {
return nil, fmt.Errorf("source database file %q doesn't exist", srcPath)
} else if err != nil {
return nil, fmt.Errorf("failed to open source database file %q: %v", srcPath, err)
}
return fi, nil
}
5 changes: 5 additions & 0 deletions tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ func (tx *Tx) Stats() TxStats {
return tx.stats
}

// Inspect returns the structure of the database.
func (tx *Tx) Inspect() BucketStructure {
return tx.root.Inspect()
}

// Bucket retrieves a bucket by name.
// Returns nil if the bucket does not exist.
// The bucket instance is only valid for the lifetime of the transaction.
Expand Down
Loading