Add RBF CLI commands

This commit is contained in:
Ben Johnson 2020-10-23 08:22:15 -06:00
parent 14cb29ea7d
commit 7fa1a5edd2
10 changed files with 901 additions and 25 deletions

126
cmd/rbf.go Normal file
View file

@ -0,0 +1,126 @@
// Copyright 2020 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"context"
"errors"
"io"
"strconv"
"github.com/pilosa/pilosa/v2/ctl"
"github.com/spf13/cobra"
)
func newRBFCommand(stdin io.Reader, stdout, stderr io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "rbf",
Short: "Inspect RBF data files.",
Long: `
Provides a set of commands for inspecting RBF data files.
`,
}
cmd.AddCommand(newRBFCheckCommand(stdin, stdout, stderr))
cmd.AddCommand(newRBFDumpCommand(stdin, stdout, stderr))
cmd.AddCommand(newRBFPagesCommand(stdin, stdout, stderr))
cmd.AddCommand(newRBFPageCommand(stdin, stdout, stderr))
return cmd
}
func newRBFCheckCommand(stdin io.Reader, stdout, stderr io.Writer) *cobra.Command {
c := ctl.NewRBFCheckCommand(stdin, stdout, stderr)
cmd := &cobra.Command{
Use: "check",
Short: "Run consistency check on RBF data.",
Long: `
Executes a consistency check on an RBF data directory.
`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c.Path = args[0]
return c.Run(context.Background())
},
}
return cmd
}
func newRBFDumpCommand(stdin io.Reader, stdout, stderr io.Writer) *cobra.Command {
c := ctl.NewRBFDumpCommand(stdin, stdout, stderr)
cmd := &cobra.Command{
Use: "dump",
Short: "Prints RBF raw page data",
Long: `
Dumps the raw hex data for one or more RBF pages.
`,
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
c.Path = args[0]
for _, arg := range args[1:] {
pgno, err := strconv.Atoi(arg)
if err != nil {
return errors.New("invalid page number")
}
c.Pgnos = append(c.Pgnos, uint32(pgno))
}
return c.Run(context.Background())
},
}
return cmd
}
func newRBFPagesCommand(stdin io.Reader, stdout, stderr io.Writer) *cobra.Command {
c := ctl.NewRBFPagesCommand(stdin, stdout, stderr)
cmd := &cobra.Command{
Use: "pages",
Short: "Prints metadata for the list of all pages",
Long: `
Prints a line for every page in the database with its type/status.
`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c.Path = args[0]
return c.Run(context.Background())
},
}
return cmd
}
func newRBFPageCommand(stdin io.Reader, stdout, stderr io.Writer) *cobra.Command {
c := ctl.NewRBFPageCommand(stdin, stdout, stderr)
cmd := &cobra.Command{
Use: "page",
Short: "Prints data for a single page",
Long: `
Prints the header & cell data for a single page.
`,
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
c.Path = args[0]
for _, arg := range args[1:] {
pgno, err := strconv.Atoi(arg)
if err != nil {
return errors.New("invalid page number")
}
c.Pgnos = append(c.Pgnos, uint32(pgno))
}
return c.Run(context.Background())
},
}
return cmd
}

View file

@ -69,6 +69,7 @@ at https://www.pilosa.com/docs/.
rc.AddCommand(newGenerateConfigCommand(stdin, stdout, stderr))
rc.AddCommand(newImportCommand(stdin, stdout, stderr))
rc.AddCommand(newInspectCommand(stdin, stdout, stderr))
rc.AddCommand(newRBFCommand(stdin, stdout, stderr))
rc.AddCommand(newServeCmd(stdin, stdout, stderr))
rc.AddCommand(newHolderCmd(stdin, stdout, stderr))

60
ctl/rbf_check.go Normal file
View file

@ -0,0 +1,60 @@
// Copyright 2017 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ctl
import (
"context"
"fmt"
"io"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/rbf"
)
// RBFCheckCommand represents a command for running a consistency check on RBF.
type RBFCheckCommand struct {
// Filepath to the RBF database.
Path string
// Standard input/output
*pilosa.CmdIO
}
// NewRBFCheckCommand returns a new instance of RBFCheckCommand.
func NewRBFCheckCommand(stdin io.Reader, stdout, stderr io.Writer) *RBFCheckCommand {
return &RBFCheckCommand{
CmdIO: pilosa.NewCmdIO(stdin, stdout, stderr),
}
}
// Run executes the export.
func (cmd *RBFCheckCommand) Run(ctx context.Context) error {
// Open database.
db := rbf.NewDB(cmd.Path)
if err := db.Open(); err != nil {
return err
}
defer db.Close()
// Run check on the database.
if err := db.Check(); err != nil {
return err
}
// If successful, print a success message.
fmt.Fprintln(cmd.Stdout, "ok")
return nil
}

111
ctl/rbf_dump.go Normal file
View file

@ -0,0 +1,111 @@
// Copyright 2017 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ctl
import (
"context"
"encoding/hex"
"fmt"
"io"
"strings"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/rbf"
)
// RBFDumpCommand represents a command for dumping raw data for an RBF page.
type RBFDumpCommand struct {
// Filepath to the RBF database.
Path string
// Page numbers to print.
Pgnos []uint32
// Standard input/output
*pilosa.CmdIO
}
// NewRBFDumpCommand returns a new instance of RBFDumpCommand.
func NewRBFDumpCommand(stdin io.Reader, stdout, stderr io.Writer) *RBFDumpCommand {
return &RBFDumpCommand{
CmdIO: pilosa.NewCmdIO(stdin, stdout, stderr),
}
}
// Run executes the export.
func (cmd *RBFDumpCommand) Run(ctx context.Context) error {
// Open database.
db := rbf.NewDB(cmd.Path)
if err := db.Open(); err != nil {
return err
}
defer db.Close()
// Execute with a transaction.
tx, err := db.Begin(false)
if err != nil {
return err
}
defer tx.Rollback()
// Fetch each page & dump.
for _, pgno := range cmd.Pgnos {
buf, err := tx.PageData(pgno)
if err != nil {
return err
}
fmt.Fprintf(cmd.Stdout, "## PAGE %d\n", pgno)
fmt.Fprintln(cmd.Stdout, compressedHexDump(buf))
fmt.Fprintln(cmd.Stdout, "")
}
return nil
}
func compressedHexDump(b []byte) string {
const prefixN = len("00000000")
var output []string
var prev string
var ellipsis bool
lines := strings.Split(strings.TrimSpace(hex.Dump(b)), "\n")
for i, line := range lines {
// Add line to output if it is not repeating or the last line.
if i == 0 || i == len(lines)-1 || trimPrefixN(line, prefixN) != trimPrefixN(prev, prefixN) {
output = append(output, line)
prev, ellipsis = line, false
continue
}
// Add an ellipsis for the first duplicate line.
if !ellipsis {
output = append(output, "...")
ellipsis = true
continue
}
}
return strings.Join(output, "\n")
}
// trimPrefixN trims n bytes from the beginning of a string.
func trimPrefixN(s string, n int) string {
if len(s) < n {
return ""
}
return s[n:]
}

140
ctl/rbf_page.go Normal file
View file

@ -0,0 +1,140 @@
// Copyright 2017 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ctl
import (
"context"
"fmt"
"io"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/rbf"
)
// RBFPageCommand represents a command for printing data for a single RBF page.
type RBFPageCommand struct {
// Filepath to the RBF database.
Path string
// Page numbers to print.
Pgnos []uint32
// Standard input/output
*pilosa.CmdIO
}
// NewRBFPageCommand returns a new instance of RBFPageCommand.
func NewRBFPageCommand(stdin io.Reader, stdout, stderr io.Writer) *RBFPageCommand {
return &RBFPageCommand{
CmdIO: pilosa.NewCmdIO(stdin, stdout, stderr),
}
}
// Run executes the export.
func (cmd *RBFPageCommand) Run(ctx context.Context) error {
// Open database.
db := rbf.NewDB(cmd.Path)
if err := db.Open(); err != nil {
return err
}
defer db.Close()
// Execute with a transaction.
tx, err := db.Begin(false)
if err != nil {
return err
}
defer tx.Rollback()
// Fetch the page.
pages, err := tx.Pages(cmd.Pgnos)
if err != nil {
return err
}
for _, page := range pages {
switch page := page.(type) {
case *rbf.MetaPage:
cmd.printMetaPage(page)
case *rbf.RootRecordPage:
cmd.printRootRecordPage(page)
case *rbf.LeafPage:
cmd.printLeafPage(page)
case *rbf.BranchPage:
cmd.printBranchPage(page)
case *rbf.BitmapPage:
cmd.printBitmapPage(page)
case *rbf.FreePage:
cmd.printFreePage(page)
default:
return fmt.Errorf("unexpected page type %T", page)
}
fmt.Fprintln(cmd.Stdout, "")
}
return nil
}
func (cmd *RBFPageCommand) printMetaPage(page *rbf.MetaPage) {
fmt.Fprintf(cmd.Stdout, "Pgno: %d\n", page.Pgno)
fmt.Fprintf(cmd.Stdout, "Type: meta\n")
fmt.Fprintf(cmd.Stdout, "PageN: %d\n", page.PageN)
fmt.Fprintf(cmd.Stdout, "WALID: %d\n", page.WALID)
fmt.Fprintf(cmd.Stdout, "Root Record Pgno: %d\n", page.RootRecordPageNo)
fmt.Fprintf(cmd.Stdout, "Freelist Pgno: %d\n", page.FreelistPageNo)
}
func (cmd *RBFPageCommand) printRootRecordPage(page *rbf.RootRecordPage) {
fmt.Fprintf(cmd.Stdout, "Pgno: %d\n", page.Pgno)
fmt.Fprintf(cmd.Stdout, "Type: root record\n")
fmt.Fprintf(cmd.Stdout, "Next: %d\n", page.Next)
fmt.Fprintf(cmd.Stdout, "Records: n=%d\n", len(page.Records))
for i, rec := range page.Records {
fmt.Fprintf(cmd.Stdout, "[%d]: name=%q pgno=%d\n", i, rec.Name, rec.Pgno)
}
}
func (cmd *RBFPageCommand) printLeafPage(page *rbf.LeafPage) {
fmt.Fprintf(cmd.Stdout, "Pgno: %d\n", page.Pgno)
fmt.Fprintf(cmd.Stdout, "Type: leaf\n")
fmt.Fprintf(cmd.Stdout, "Cells: n=%d\n", len(page.Cells))
for i, cell := range page.Cells {
if cell.Type == "bitmap-ptr" {
fmt.Fprintf(cmd.Stdout, "[%d]: key=%d type=%s pgno=%d\n", i, cell.Key, cell.Type, cell.Pgno)
} else {
fmt.Fprintf(cmd.Stdout, "[%d]: key=%d type=%s values=%v\n", i, cell.Key, cell.Type, cell.Values)
}
}
}
func (cmd *RBFPageCommand) printBranchPage(page *rbf.BranchPage) {
fmt.Fprintf(cmd.Stdout, "Pgno: %d\n", page.Pgno)
fmt.Fprintf(cmd.Stdout, "Type: branch\n")
fmt.Fprintf(cmd.Stdout, "Cells: n=%d\n", len(page.Cells))
for i, cell := range page.Cells {
fmt.Fprintf(cmd.Stdout, "[%d]: key=%d flags=%d pgno=%d\n", i, cell.Key, cell.Flags, cell.Pgno)
}
}
func (cmd *RBFPageCommand) printBitmapPage(page *rbf.BitmapPage) {
fmt.Fprintf(cmd.Stdout, "Pgno: %d\n", page.Pgno)
fmt.Fprintf(cmd.Stdout, "Type: bitmap\n")
fmt.Fprintf(cmd.Stdout, "Values: %v\n", page.Values)
}
func (cmd *RBFPageCommand) printFreePage(page *rbf.FreePage) {
fmt.Fprintf(cmd.Stdout, "Pgno: %d\n", page.Pgno)
fmt.Fprintf(cmd.Stdout, "Type: free\n")
}

99
ctl/rbf_pages.go Normal file
View file

@ -0,0 +1,99 @@
// Copyright 2017 Pilosa Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ctl
import (
"context"
"fmt"
"io"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/rbf"
"github.com/pilosa/pilosa/v2/txkey"
)
// RBFPagesCommand represents a command for printing a list of RBF page metadata.
type RBFPagesCommand struct {
// Filepath to the RBF database.
Path string
// Standard input/output
*pilosa.CmdIO
}
// NewRBFPagesCommand returns a new instance of RBFPagesCommand.
func NewRBFPagesCommand(stdin io.Reader, stdout, stderr io.Writer) *RBFPagesCommand {
return &RBFPagesCommand{
CmdIO: pilosa.NewCmdIO(stdin, stdout, stderr),
}
}
// Run executes the export.
func (cmd *RBFPagesCommand) Run(ctx context.Context) error {
// Open database.
db := rbf.NewDB(cmd.Path)
if err := db.Open(); err != nil {
return err
}
defer db.Close()
// Execute with a transaction.
tx, err := db.Begin(false)
if err != nil {
return err
}
defer tx.Rollback()
// Iterate over each page and grab info.
infos, err := tx.PageInfos()
if err != nil {
return err
}
// Write header.
fmt.Fprintln(cmd.Stdout, "ID TYPE TREE EXTRA")
fmt.Fprintln(cmd.Stdout, "======== ========== ============================== ====================")
// Print one line for each page.
for pgno, info := range infos {
switch info := info.(type) {
case *rbf.MetaPageInfo:
fmt.Fprintf(cmd.Stdout, "%-8d %-10s %-30q pageN=%d,walid=%d,rootrec=%d,freelist=%d\n", pgno, "meta", "", info.PageN, info.WALID, info.RootRecordPageNo, info.FreelistPageNo)
case *rbf.RootRecordPageInfo:
fmt.Fprintf(cmd.Stdout, "%-8d %-10s %-30q next=%d\n", pgno, "rootrec", "", info.Next)
case *rbf.LeafPageInfo:
fmt.Fprintf(cmd.Stdout, "%-8d %-10s %-30q flags=x%x,celln=%d\n", pgno, "leaf", txkeyString(info.Tree), info.Flags, info.CellN)
case *rbf.BranchPageInfo:
fmt.Fprintf(cmd.Stdout, "%-8d %-10s %-30q flags=x%x,celln=%d\n", pgno, "branch", txkeyString(info.Tree), info.Flags, info.CellN)
case *rbf.BitmapPageInfo:
fmt.Fprintf(cmd.Stdout, "%-8d %-10s %-30q -\n", pgno, "bitmap", info.Tree)
case *rbf.FreePageInfo:
fmt.Fprintf(cmd.Stdout, "%-8d %-10s %-30q -\n", pgno, "free", "")
default:
panic(fmt.Sprintf("unexpected page info type %T", info))
}
}
return nil
}
func txkeyString(s string) (ret string) {
defer func() {
if err := recover(); err != nil {
ret = s
}
}()
return txkey.ToString([]byte(s))
}

View file

@ -807,7 +807,7 @@ type EasyWalker struct {
func (e *EasyWalker) Visitor(pgno uint32, records []*rbf.RootRecord) {
for _, record := range records {
e.VisitRoot(record.Pgno, record.Name)
rbf.Page(e.tx, record.Pgno, e)
rbf.WalkPage(e.tx, record.Pgno, e)
}
}
func (e *EasyWalker) VisitRoot(pgno uint32, name string) {
@ -927,7 +927,7 @@ func TestCursor_SplitBranchCells(t *testing.T) {
}
}
before := &EasyWalker{tx: tx}
rbf.Page(tx, 0, before)
rbf.WalkPage(tx, 0, before)
if before.String() != "RL" {
t.Fatalf("Expecting RL (one branch) got %v", before.String())
@ -939,7 +939,7 @@ func TestCursor_SplitBranchCells(t *testing.T) {
}
after := &EasyWalker{tx: tx}
rbf.Page(tx, 0, after)
rbf.WalkPage(tx, 0, after)
if after.String() != "RBLL" {
t.Fatalf("Expecting RBLL (a branch split) got %v", after.String())

View file

@ -215,7 +215,7 @@ type Walker interface {
Visit(pgno uint32, n Nodetype)
}
func Page(tx *Tx, pgno uint32, walker Walker) {
func WalkPage(tx *Tx, pgno uint32, walker Walker) {
page, err := tx.readPage(pgno)
if err != nil {
panic(err)
@ -232,7 +232,7 @@ func Page(tx *Tx, pgno uint32, walker Walker) {
walker.Visit(pgno, Branch)
for i, n := 0, readCellN(page); i < n; i++ {
cell := readBranchCell(page, i)
Page(tx, cell.Pgno, walker)
WalkPage(tx, cell.Pgno, walker)
}
case PageTypeLeaf:
walker.Visit(pgno, Leaf)

View file

@ -55,7 +55,8 @@ const (
PageTypeRootRecord = 1
PageTypeLeaf = 2
PageTypeBranch = 4
PageTypeBitmapHeader = 8 // Only used by the WAL for marking next page
PageTypeBitmapHeader = 8 // Only used by the WAL for marking next page
PageTypeBitmap = 16 // Only used internally when walking the b-tree
)
// Meta commit/rollback flags.
@ -73,6 +74,24 @@ const (
ContainerTypeBitmapPtr
)
// ContainerTypeString returns a string representation of the container type.
func ContainerTypeString(typ int) string {
switch typ {
case ContainerTypeNone:
return "none"
case ContainerTypeArray:
return "array"
case ContainerTypeRLE:
return "rle"
case ContainerTypeBitmap:
return "bitmap"
case ContainerTypeBitmapPtr:
return "bitmap-ptr"
default:
return fmt.Sprintf("unknown<%d>", typ)
}
}
const (
rootRecordPageHeaderSize = 12
rootRecordHeaderSize = 4 + 2 // pgno, len(name)
@ -333,16 +352,8 @@ func (c *leafCell) Values(tx *Tx) []uint16 {
a = a[:n]
return a
case ContainerTypeBitmapPtr:
a := make([]uint16, 0, BitmapN*64)
_, bm, _ := tx.leafCellBitmap(toPgno(c.Data))
for i, v := range bm {
for j := uint(0); j < 64; j++ {
if v&(1<<j) != 0 {
a = append(a, (uint16(i)*64)+uint16(j))
}
}
}
return a
return bitmapValues(bm)
case ContainerTypeNone:
return []uint16{}
default:
@ -350,6 +361,18 @@ func (c *leafCell) Values(tx *Tx) []uint16 {
}
}
func bitmapValues(bm []uint64) []uint16 {
a := make([]uint16, 0, BitmapN*64)
for i, v := range bm {
for j := uint(0); j < 64; j++ {
if v&(1<<j) != 0 {
a = append(a, (uint16(i)*64)+uint16(j))
}
}
}
return a
}
// firstValue the first value from the container.
func (c *leafCell) firstValue(tx *Tx) uint16 {
switch c.Type {

336
rbf/tx.go
View file

@ -66,6 +66,11 @@ func (tx *Tx) Writable() bool {
return tx.writable
}
// PageN returns the number of pages in the database as seen by this transaction.
func (tx *Tx) PageN() int {
return int(readMetaPageN(tx.meta[:]))
}
// Commit completes the transaction and persists data changes.
func (tx *Tx) Commit() error {
tx.mu.Lock()
@ -788,7 +793,7 @@ func (tx *Tx) inusePageSet() (map[uint32]struct{}, error) {
}
// Traverse freelist and mark pages as in-use.
if err := tx.walkTree(readMetaFreelistPageNo(tx.meta[:]), func(pgno uint32) error {
if err := tx.walkTree(readMetaFreelistPageNo(tx.meta[:]), 0, func(pgno, parent, typ uint32) error {
m[pgno] = struct{}{}
return nil
}); err != nil {
@ -801,7 +806,7 @@ func (tx *Tx) inusePageSet() (map[uint32]struct{}, error) {
return m, err
}
for _, record := range records {
if err := tx.walkTree(record.Pgno, func(pgno uint32) error {
if err := tx.walkTree(record.Pgno, 0, func(pgno, parent, typ uint32) error {
m[pgno] = struct{}{}
return nil
}); err != nil {
@ -813,28 +818,37 @@ func (tx *Tx) inusePageSet() (map[uint32]struct{}, error) {
}
// walkTree recursively iterates over a page and all its children.
func (tx *Tx) walkTree(pgno uint32, fn func(uint32) error) error {
// Execute callback.
if err := fn(pgno); err != nil {
return err
}
func (tx *Tx) walkTree(pgno, parent uint32, fn func(pgno, parent, typ uint32) error) error {
// Read page and iterate over children.
page, err := tx.readPage(pgno)
if err != nil {
return err
}
switch typ := readFlags(page); typ {
// Execute callback.
typ := readFlags(page)
if err := fn(pgno, parent, typ); err != nil {
return err
}
switch typ {
case PageTypeBranch:
for i, n := 0, readCellN(page); i < n; i++ {
cell := readBranchCell(page, i)
if err := tx.walkTree(cell.Pgno, fn); err != nil {
if err := tx.walkTree(cell.Pgno, pgno, fn); err != nil {
return err
}
}
return nil
case PageTypeLeaf:
// Execute callback only for bitmap pages pointed to by this leaf.
for i, n := 0, readCellN(page); i < n; i++ {
if cell := readLeafCell(page, i); cell.Type == ContainerTypeBitmapPtr {
if err := fn(toPgno(cell.Data), pgno, PageTypeBitmap); err != nil {
return err
}
}
}
return nil
default:
return fmt.Errorf("rbf.Tx.forEachTreePage(): invalid page type: pgno=%d type=%d", pgno, typ)
@ -1699,3 +1713,305 @@ func (tx *Tx) ensureWritableWALSegment() error {
return nil
}
// Pages returns meta & record data for a list of pages.
func (tx *Tx) Pages(pgnos []uint32) ([]Page, error) {
// Read page info for all pages in the database.
infos, err := tx.PageInfos()
if err != nil {
return nil, err
}
// Loop over each requested page number and extract additional data.
var pages []Page
for _, pgno := range pgnos {
buf, err := tx.readPage(pgno)
if err != nil {
return nil, err
}
switch info := infos[pgno].(type) {
case *MetaPageInfo:
pages = append(pages, &MetaPage{MetaPageInfo: info})
case *RootRecordPageInfo:
records, err := readRootRecords(buf)
if err != nil {
return nil, err
}
pages = append(pages, &RootRecordPage{RootRecordPageInfo: info, Records: records})
case *LeafPageInfo:
page := &LeafPage{LeafPageInfo: info}
cells := make([]leafCell, page.CellN)
for _, cell := range readLeafCells(buf, cells) {
other := &LeafCell{
Key: cell.Key,
Type: ContainerTypeString(cell.Type),
}
switch cell.Type {
case ContainerTypeArray, ContainerTypeRLE:
other.Values = cell.Values(tx)
case ContainerTypeBitmapPtr:
other.Pgno = toPgno(cell.Data)
}
page.Cells = append(page.Cells, other)
}
pages = append(pages, page)
case *BranchPageInfo:
page := &BranchPage{BranchPageInfo: info}
for _, cell := range readBranchCells(buf) {
page.Cells = append(page.Cells, &BranchCell{
Key: cell.Key,
Flags: cell.Flags,
Pgno: cell.Pgno,
})
}
pages = append(pages, page)
case *BitmapPageInfo:
pages = append(pages, &BitmapPage{
BitmapPageInfo: info,
Values: bitmapValues(toArray64(buf)),
})
case *FreePageInfo:
pages = append(pages, &FreePage{FreePageInfo: info})
default:
panic(fmt.Sprintf("invalid page info type %T", info))
}
}
return pages, nil
}
// PageInfos returns meta data about all pages in the database.
func (tx *Tx) PageInfos() ([]PageInfo, error) {
infos := make([]PageInfo, tx.PageN())
// Read meta page info.
metaInfo, err := tx.metaPageInfo()
if err != nil {
return nil, err
}
infos[0] = metaInfo
// Traverse root record linked list.
for pgno := metaInfo.RootRecordPageNo; pgno != 0; {
info, err := tx.rootRecordPageInfo(pgno)
if err != nil {
return nil, err
}
infos[pgno] = info
pgno = info.Next
}
// Traverse freelist and mark pages as in-use.
if err := tx.walkPageInfo(infos, metaInfo.FreelistPageNo, "freelist"); err != nil {
return nil, err
}
// Traverse every b-tree and mark pages as in-use.
records, err := tx.RootRecords()
if err != nil {
return nil, err
}
for _, record := range records {
if err := tx.walkPageInfo(infos, record.Pgno, record.Name); err != nil {
return nil, err
}
}
// Build page info objects for each free page.
freePageSet, err := tx.freePageSet()
if err != nil {
return nil, err
}
for pgno := range freePageSet {
infos[pgno] = &FreePageInfo{Pgno: pgno}
}
return infos, nil
}
// metaPageInfo returns page metadata for the meta page.
func (tx *Tx) metaPageInfo() (*MetaPageInfo, error) {
buf, err := tx.readPage(0)
if err != nil {
return nil, err
}
return &MetaPageInfo{
Pgno: 0,
Magic: readMetaMagic(buf),
PageN: readMetaPageN(buf),
WALID: readMetaWALID(buf),
RootRecordPageNo: readMetaRootRecordPageNo(buf),
FreelistPageNo: readMetaFreelistPageNo(buf),
}, nil
}
// rootRecordPageInfo returns page metadata for a root record page.
func (tx *Tx) rootRecordPageInfo(pgno uint32) (*RootRecordPageInfo, error) {
buf, err := tx.readPage(pgno)
if err != nil {
return nil, err
}
return &RootRecordPageInfo{
Pgno: pgno,
Next: WalkRootRecordPages(buf),
}, nil
}
func (tx *Tx) walkPageInfo(infos []PageInfo, root uint32, name string) error {
return tx.walkTree(root, 0, func(pgno, parent, typ uint32) error {
buf, err := tx.readPage(pgno)
if err != nil {
return err
}
switch typ {
case PageTypeLeaf:
infos[pgno] = &LeafPageInfo{
Pgno: pgno,
Parent: parent,
Tree: name,
Flags: readFlags(buf),
CellN: readCellN(buf),
}
case PageTypeBranch:
infos[pgno] = &BranchPageInfo{
Pgno: pgno,
Parent: parent,
Tree: name,
Flags: readFlags(buf),
CellN: readCellN(buf),
}
case PageTypeBitmap:
infos[pgno] = &BitmapPageInfo{
Pgno: pgno,
Parent: parent,
Tree: name,
}
default:
panic(fmt.Sprintf("unexpected page type %d for page %d", typ, pgno))
}
return nil
})
}
// PageData returns the raw page data for a single page.
func (tx *Tx) PageData(pgno uint32) ([]byte, error) {
return tx.readPage(pgno)
}
type PageInfo interface {
pageInfo()
}
func (*MetaPageInfo) pageInfo() {}
func (*RootRecordPageInfo) pageInfo() {}
func (*LeafPageInfo) pageInfo() {}
func (*BranchPageInfo) pageInfo() {}
func (*BitmapPageInfo) pageInfo() {}
func (*FreePageInfo) pageInfo() {}
type MetaPageInfo struct {
Pgno uint32
Magic []byte
PageN uint32
WALID int64
RootRecordPageNo uint32
FreelistPageNo uint32
}
type RootRecordPageInfo struct {
Pgno uint32
Next uint32
}
type LeafPageInfo struct {
Pgno uint32
Parent uint32
Tree string
Flags uint32
CellN int
}
type BranchPageInfo struct {
Pgno uint32
Parent uint32
Tree string
Flags uint32
CellN int
}
type BitmapPageInfo struct {
Pgno uint32
Parent uint32
Tree string
}
type FreePageInfo struct {
Pgno uint32
}
type Page interface {
page()
}
func (*MetaPage) page() {}
func (*RootRecordPage) page() {}
func (*LeafPage) page() {}
func (*BranchPage) page() {}
func (*BitmapPage) page() {}
func (*FreePage) page() {}
type MetaPage struct {
*MetaPageInfo
}
type RootRecordPage struct {
*RootRecordPageInfo
Records []*RootRecord
}
type LeafPage struct {
*LeafPageInfo
Cells []*LeafCell
}
// LeafCell represents a leaf cell in the public API.
type LeafCell struct {
Key uint64
Type string // container type
Pgno uint32 // bitmap pointer only
Values []uint16 // array & rle containers only
}
type BranchPage struct {
*BranchPageInfo
Cells []*BranchCell
}
// BranchCell represents a branch cell in the public API.
type BranchCell struct {
Key uint64
Flags uint32
Pgno uint32
}
type BitmapPage struct {
*BitmapPageInfo
Values []uint16
}
type FreePage struct {
*FreePageInfo
}