From e618196323d77e4eab6f7e54d2c5aa233a8d5226 Mon Sep 17 00:00:00 2001 From: Benjamin Wang Date: Thu, 4 May 2023 15:53:45 +0800 Subject: [PATCH] cmd: migrate 'surgery copy-page' command to cobra style comamnd Signed-off-by: Benjamin Wang --- cmd/bbolt/command_surgery_cobra.go | 56 ++++++++++++++++++++-- cmd/bbolt/command_surgery_cobra_test.go | 59 +++++++++++++++++++++++ cmd/bbolt/surgery_commands.go | 64 ------------------------- cmd/bbolt/surgery_commands_test.go | 54 --------------------- 4 files changed, 111 insertions(+), 122 deletions(-) diff --git a/cmd/bbolt/command_surgery_cobra.go b/cmd/bbolt/command_surgery_cobra.go index a62bec40a..7c9757c5a 100644 --- a/cmd/bbolt/command_surgery_cobra.go +++ b/cmd/bbolt/command_surgery_cobra.go @@ -18,10 +18,12 @@ var ( ) var ( - surgeryTargetDBFilePath string - surgeryPageId uint64 - surgeryStartElementIdx int - surgeryEndElementIdx int + surgeryTargetDBFilePath string + surgeryPageId uint64 + surgeryStartElementIdx int + surgeryEndElementIdx int + surgerySourcePageId uint64 + surgeryDestinationPageId uint64 ) func newSurgeryCobraCommand() *cobra.Command { @@ -31,6 +33,7 @@ func newSurgeryCobraCommand() *cobra.Command { } surgeryCmd.AddCommand(newSurgeryRevertMetaPageCommand()) + surgeryCmd.AddCommand(newSurgeryCopyPageCommand()) surgeryCmd.AddCommand(newSurgeryClearPageElementsCommand()) surgeryCmd.AddCommand(newSurgeryFreelistCommand()) @@ -78,6 +81,51 @@ func surgeryRevertMetaPageFunc(cmd *cobra.Command, args []string) error { return nil } +func newSurgeryCopyPageCommand() *cobra.Command { + copyPageCmd := &cobra.Command{ + Use: "copy-page [options]", + Short: "Copy page from the source page Id to the destination page Id", + 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: surgeryCopyPageFunc, + } + + copyPageCmd.Flags().StringVar(&surgeryTargetDBFilePath, "output", "", "path to the target db file") + copyPageCmd.Flags().Uint64VarP(&surgerySourcePageId, "from-page", "", 0, "source page Id") + copyPageCmd.Flags().Uint64VarP(&surgeryDestinationPageId, "to-page", "", 0, "destination page Id") + + return copyPageCmd +} + +func surgeryCopyPageFunc(cmd *cobra.Command, args []string) error { + srcDBPath := args[0] + + if surgerySourcePageId == surgeryDestinationPageId { + return fmt.Errorf("'--from-page' and '--to-page' have the same value: %d", surgerySourcePageId) + } + + if err := common.CopyFile(srcDBPath, surgeryTargetDBFilePath); err != nil { + return fmt.Errorf("[copy-page] copy file failed: %w", err) + } + + if err := surgeon.CopyPage(surgeryTargetDBFilePath, common.Pgid(surgerySourcePageId), common.Pgid(surgeryDestinationPageId)); err != nil { + return fmt.Errorf("copy-page command failed: %w", err) + } + + fmt.Fprintf(os.Stdout, "WARNING: the free list might have changed.\n") + fmt.Fprintf(os.Stdout, "Please consider executing `./bbolt surgery abandon-freelist ...`\n") + + fmt.Fprintf(os.Stdout, "The page %d was successfully copied to page %d\n", surgerySourcePageId, surgeryDestinationPageId) + return nil +} + func newSurgeryClearPageElementsCommand() *cobra.Command { clearElementCmd := &cobra.Command{ Use: "clear-page-elements [options]", diff --git a/cmd/bbolt/command_surgery_cobra_test.go b/cmd/bbolt/command_surgery_cobra_test.go index fc0f6376e..5c506a91a 100644 --- a/cmd/bbolt/command_surgery_cobra_test.go +++ b/cmd/bbolt/command_surgery_cobra_test.go @@ -61,6 +61,44 @@ func TestSurgery_RevertMetaPage(t *testing.T) { assert.Equal(t, pageDataWithoutPageId(nonActiveSrcBuf), pageDataWithoutPageId(dstBuf1)) } +func TestSurgery_CopyPage(t *testing.T) { + pageSize := 4096 + db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: pageSize}) + srcPath := db.Path() + + // Insert some sample data + t.Log("Insert some sample data") + err := db.Fill([]byte("data"), 1, 20, + func(tx int, k int) []byte { return []byte(fmt.Sprintf("%04d", k)) }, + func(tx int, k int) []byte { return make([]byte, 10) }, + ) + require.NoError(t, err) + + defer requireDBNoChange(t, dbData(t, srcPath), srcPath) + + // copy page 3 to page 2 + t.Log("copy page 3 to page 2") + rootCmd := main.NewRootCommand() + output := filepath.Join(t.TempDir(), "dstdb") + rootCmd.SetArgs([]string{ + "surgery", "copy-page", srcPath, + "--output", output, + "--from-page", "3", + "--to-page", "2", + }) + err = rootCmd.Execute() + require.NoError(t, err) + + // The page 2 should have exactly the same data as page 3. + t.Log("Verify result") + srcPageId3Data := readPage(t, srcPath, 3, pageSize) + dstPageId3Data := readPage(t, output, 3, pageSize) + dstPageId2Data := readPage(t, output, 2, pageSize) + + assert.Equal(t, srcPageId3Data, dstPageId3Data) + assert.Equal(t, pageDataWithoutPageId(srcPageId3Data), pageDataWithoutPageId(dstPageId2Data)) +} + func TestSurgery_ClearPageElements_Without_Overflow(t *testing.T) { testCases := []struct { name string @@ -578,3 +616,24 @@ func readMetaPage(t *testing.T, path string) *common.Meta { require.NoError(t, err) return common.LoadPageMeta(buf) } + +func readPage(t *testing.T, path string, pageId int, pageSize int) []byte { + dbFile, err := os.Open(path) + require.NoError(t, err) + defer dbFile.Close() + + fi, err := dbFile.Stat() + require.NoError(t, err) + require.GreaterOrEqual(t, fi.Size(), int64((pageId+1)*pageSize)) + + buf := make([]byte, pageSize) + byteRead, err := dbFile.ReadAt(buf, int64(pageId*pageSize)) + require.NoError(t, err) + require.Equal(t, pageSize, byteRead) + + return buf +} + +func pageDataWithoutPageId(buf []byte) []byte { + return buf[8:] +} diff --git a/cmd/bbolt/surgery_commands.go b/cmd/bbolt/surgery_commands.go index 385903134..64b970ae3 100644 --- a/cmd/bbolt/surgery_commands.go +++ b/cmd/bbolt/surgery_commands.go @@ -40,8 +40,6 @@ func (cmd *surgeryCommand) Run(args ...string) error { case "help": fmt.Fprintln(cmd.Stderr, cmd.Usage()) return ErrUsage - case "copy-page": - return newCopyPageCommand(cmd).Run(args[1:]...) case "clear-page": return newClearPageCommand(cmd).Run(args[1:]...) default: @@ -81,73 +79,11 @@ Usage: The commands are: help print this screen clear-page clear all elements at the given pageId - copy-page copy page from source pageId to target pageId - revert-meta-page revert the meta page change made by the last transaction Use "bbolt surgery [command] -h" for more information about a command. `, "\n") } -// copyPageCommand represents the "surgery copy-page" command execution. -type copyPageCommand struct { - *surgeryCommand -} - -// newCopyPageCommand returns a copyPageCommand. -func newCopyPageCommand(m *surgeryCommand) *copyPageCommand { - c := ©PageCommand{} - c.surgeryCommand = m - return c -} - -// Run executes the command. -func (cmd *copyPageCommand) Run(args ...string) error { - // Parse flags. - fs := flag.NewFlagSet("", flag.ContinueOnError) - help := fs.Bool("h", false, "") - if err := fs.Parse(args); err != nil { - return err - } else if *help { - fmt.Fprintln(cmd.Stderr, cmd.Usage()) - return ErrUsage - } - - if err := cmd.parsePathsAndCopyFile(fs); err != nil { - return fmt.Errorf("copyPageCommand failed to parse paths and copy file: %w", err) - } - - // Read page id. - srcPageId, err := strconv.ParseUint(fs.Arg(2), 10, 64) - if err != nil { - return err - } - dstPageId, err := strconv.ParseUint(fs.Arg(3), 10, 64) - if err != nil { - return err - } - - // copy the page - if err := surgeon.CopyPage(cmd.dstPath, common.Pgid(srcPageId), common.Pgid(dstPageId)); err != nil { - return fmt.Errorf("copyPageCommand failed: %w", err) - } - - fmt.Fprintf(cmd.Stdout, "The page %d was copied to page %d\n", srcPageId, dstPageId) - return nil -} - -// Usage returns the help message. -func (cmd *copyPageCommand) Usage() string { - return strings.TrimLeft(` -usage: bolt surgery copy-page SRC DST srcPageId dstPageid - -CopyPage copies the database file at SRC to a newly created database -file at DST. Afterwards, it copies the page at srcPageId to the page -at dstPageId in DST. - -The original database is left untouched. -`, "\n") -} - // clearPageCommand represents the "surgery clear-page" command execution. type clearPageCommand struct { *surgeryCommand diff --git a/cmd/bbolt/surgery_commands_test.go b/cmd/bbolt/surgery_commands_test.go index 567c0c493..af3b1393e 100644 --- a/cmd/bbolt/surgery_commands_test.go +++ b/cmd/bbolt/surgery_commands_test.go @@ -2,7 +2,6 @@ package main_test import ( "fmt" - "os" "path/filepath" "testing" @@ -14,38 +13,6 @@ import ( "go.etcd.io/bbolt/internal/common" ) -func TestSurgery_CopyPage(t *testing.T) { - pageSize := 4096 - db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: pageSize}) - srcPath := db.Path() - - // Insert some sample data - t.Log("Insert some sample data") - err := db.Fill([]byte("data"), 1, 20, - func(tx int, k int) []byte { return []byte(fmt.Sprintf("%04d", k)) }, - func(tx int, k int) []byte { return make([]byte, 10) }, - ) - require.NoError(t, err) - - defer requireDBNoChange(t, dbData(t, srcPath), srcPath) - - // copy page 3 to page 2 - t.Log("copy page 3 to page 2") - dstPath := filepath.Join(t.TempDir(), "dstdb") - m := NewMain() - err = m.Run("surgery", "copy-page", srcPath, dstPath, "3", "2") - require.NoError(t, err) - - // The page 2 should have exactly the same data as page 3. - t.Log("Verify result") - srcPageId3Data := readPage(t, srcPath, 3, pageSize) - dstPageId3Data := readPage(t, dstPath, 3, pageSize) - dstPageId2Data := readPage(t, dstPath, 2, pageSize) - - assert.Equal(t, srcPageId3Data, dstPageId3Data) - assert.Equal(t, pageDataWithoutPageId(srcPageId3Data), pageDataWithoutPageId(dstPageId2Data)) -} - // TODO(ahrtr): add test case below for `surgery clear-page` command: // 1. The page is a branch page. All its children should become free pages. func TestSurgery_ClearPage(t *testing.T) { @@ -78,24 +45,3 @@ func TestSurgery_ClearPage(t *testing.T) { assert.Equal(t, uint16(0), p.Count()) assert.Equal(t, uint32(0), p.Overflow()) } - -func readPage(t *testing.T, path string, pageId int, pageSize int) []byte { - dbFile, err := os.Open(path) - require.NoError(t, err) - defer dbFile.Close() - - fi, err := dbFile.Stat() - require.NoError(t, err) - require.GreaterOrEqual(t, fi.Size(), int64((pageId+1)*pageSize)) - - buf := make([]byte, pageSize) - byteRead, err := dbFile.ReadAt(buf, int64(pageId*pageSize)) - require.NoError(t, err) - require.Equal(t, pageSize, byteRead) - - return buf -} - -func pageDataWithoutPageId(buf []byte) []byte { - return buf[8:] -}