LCOV - code coverage report
Current view: top level - pebble/tool - db.go (source / functions) Hit Total Coverage
Test: 2024-04-20 08:15Z 5b3f94f1 - tests + meta.lcov Lines: 602 711 84.7 %
Date: 2024-04-20 08:16:32 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
       2             : // of this source code is governed by a BSD-style license that can be found in
       3             : // the LICENSE file.
       4             : 
       5             : package tool
       6             : 
       7             : import (
       8             :         "bufio"
       9             :         "context"
      10             :         "fmt"
      11             :         "io"
      12             :         "math/rand"
      13             :         "strings"
      14             :         "text/tabwriter"
      15             : 
      16             :         "github.com/cockroachdb/errors"
      17             :         "github.com/cockroachdb/errors/oserror"
      18             :         "github.com/cockroachdb/pebble"
      19             :         "github.com/cockroachdb/pebble/internal/base"
      20             :         "github.com/cockroachdb/pebble/internal/humanize"
      21             :         "github.com/cockroachdb/pebble/internal/manifest"
      22             :         "github.com/cockroachdb/pebble/objstorage"
      23             :         "github.com/cockroachdb/pebble/objstorage/objstorageprovider"
      24             :         "github.com/cockroachdb/pebble/record"
      25             :         "github.com/cockroachdb/pebble/sstable"
      26             :         "github.com/cockroachdb/pebble/tool/logs"
      27             :         "github.com/cockroachdb/pebble/vfs"
      28             :         "github.com/spf13/cobra"
      29             : )
      30             : 
      31             : // dbT implements db-level tools, including both configuration state and the
      32             : // commands themselves.
      33             : type dbT struct {
      34             :         Root       *cobra.Command
      35             :         Check      *cobra.Command
      36             :         Checkpoint *cobra.Command
      37             :         Get        *cobra.Command
      38             :         Logs       *cobra.Command
      39             :         LSM        *cobra.Command
      40             :         Properties *cobra.Command
      41             :         Scan       *cobra.Command
      42             :         Set        *cobra.Command
      43             :         Space      *cobra.Command
      44             :         IOBench    *cobra.Command
      45             :         Excise     *cobra.Command
      46             : 
      47             :         // Configuration.
      48             :         opts            *pebble.Options
      49             :         comparers       sstable.Comparers
      50             :         mergers         sstable.Mergers
      51             :         openErrEnhancer func(error) error
      52             :         openOptions     []OpenOption
      53             :         exciseSpanFn    DBExciseSpanFn
      54             : 
      55             :         // Flags.
      56             :         comparerName  string
      57             :         mergerName    string
      58             :         fmtKey        keyFormatter
      59             :         fmtValue      valueFormatter
      60             :         start         key
      61             :         end           key
      62             :         count         int64
      63             :         allLevels     bool
      64             :         ioCount       int
      65             :         ioParallelism int
      66             :         ioSizes       string
      67             :         verbose       bool
      68             :         bypassPrompt  bool
      69             : }
      70             : 
      71             : func newDB(
      72             :         opts *pebble.Options,
      73             :         comparers sstable.Comparers,
      74             :         mergers sstable.Mergers,
      75             :         openErrEnhancer func(error) error,
      76             :         openOptions []OpenOption,
      77             :         exciseSpanFn DBExciseSpanFn,
      78           1 : ) *dbT {
      79           1 :         d := &dbT{
      80           1 :                 opts:            opts,
      81           1 :                 comparers:       comparers,
      82           1 :                 mergers:         mergers,
      83           1 :                 openErrEnhancer: openErrEnhancer,
      84           1 :                 openOptions:     openOptions,
      85           1 :                 exciseSpanFn:    exciseSpanFn,
      86           1 :         }
      87           1 :         d.fmtKey.mustSet("quoted")
      88           1 :         d.fmtValue.mustSet("[%x]")
      89           1 : 
      90           1 :         d.Root = &cobra.Command{
      91           1 :                 Use:   "db",
      92           1 :                 Short: "DB introspection tools",
      93           1 :         }
      94           1 :         d.Check = &cobra.Command{
      95           1 :                 Use:   "check <dir>",
      96           1 :                 Short: "verify checksums and metadata",
      97           1 :                 Long: `
      98           1 : Verify sstable, manifest, and WAL checksums. Requires that the specified
      99           1 : database not be in use by another process.
     100           1 : `,
     101           1 :                 Args: cobra.ExactArgs(1),
     102           1 :                 Run:  d.runCheck,
     103           1 :         }
     104           1 :         d.Checkpoint = &cobra.Command{
     105           1 :                 Use:   "checkpoint <src-dir> <dest-dir>",
     106           1 :                 Short: "create a checkpoint",
     107           1 :                 Long: `
     108           1 : Creates a Pebble checkpoint in the specified destination directory. A checkpoint
     109           1 : is a point-in-time snapshot of DB state. Requires that the specified
     110           1 : database not be in use by another process.
     111           1 : `,
     112           1 :                 Args: cobra.ExactArgs(2),
     113           1 :                 Run:  d.runCheckpoint,
     114           1 :         }
     115           1 :         d.Get = &cobra.Command{
     116           1 :                 Use:   "get <dir> <key>",
     117           1 :                 Short: "get value for a key",
     118           1 :                 Long: `
     119           1 : Gets a value for a key, if it exists in DB. Prints a "not found" error if key
     120           1 : does not exist. Requires that the specified database not be in use by another
     121           1 : process.
     122           1 : `,
     123           1 :                 Args: cobra.ExactArgs(2),
     124           1 :                 Run:  d.runGet,
     125           1 :         }
     126           1 :         d.Logs = logs.NewCmd()
     127           1 :         d.LSM = &cobra.Command{
     128           1 :                 Use:   "lsm <dir>",
     129           1 :                 Short: "print LSM structure",
     130           1 :                 Long: `
     131           1 : Print the structure of the LSM tree. Requires that the specified database not
     132           1 : be in use by another process.
     133           1 : `,
     134           1 :                 Args: cobra.ExactArgs(1),
     135           1 :                 Run:  d.runLSM,
     136           1 :         }
     137           1 :         d.Properties = &cobra.Command{
     138           1 :                 Use:   "properties <dir>",
     139           1 :                 Short: "print aggregated sstable properties",
     140           1 :                 Long: `
     141           1 : Print SSTable properties, aggregated per level of the LSM.
     142           1 : `,
     143           1 :                 Args: cobra.ExactArgs(1),
     144           1 :                 Run:  d.runProperties,
     145           1 :         }
     146           1 :         d.Scan = &cobra.Command{
     147           1 :                 Use:   "scan <dir>",
     148           1 :                 Short: "print db records",
     149           1 :                 Long: `
     150           1 : Print the records in the DB. Requires that the specified database not be in use
     151           1 : by another process.
     152           1 : `,
     153           1 :                 Args: cobra.ExactArgs(1),
     154           1 :                 Run:  d.runScan,
     155           1 :         }
     156           1 :         d.Set = &cobra.Command{
     157           1 :                 Use:   "set <dir> <key> <value>",
     158           1 :                 Short: "set a value for a key",
     159           1 :                 Long: `
     160           1 : Adds a new key/value to the DB. Requires that the specified database
     161           1 : not be in use by another process.
     162           1 : `,
     163           1 :                 Args: cobra.ExactArgs(3),
     164           1 :                 Run:  d.runSet,
     165           1 :         }
     166           1 :         d.Space = &cobra.Command{
     167           1 :                 Use:   "space <dir>",
     168           1 :                 Short: "print filesystem space used",
     169           1 :                 Long: `
     170           1 : Print the estimated filesystem space usage for the inclusive-inclusive range
     171           1 : specified by --start and --end. Requires that the specified database not be in
     172           1 : use by another process.
     173           1 : `,
     174           1 :                 Args: cobra.ExactArgs(1),
     175           1 :                 Run:  d.runSpace,
     176           1 :         }
     177           1 :         d.Excise = &cobra.Command{
     178           1 :                 Use:   "excise <dir>",
     179           1 :                 Short: "excise a key range",
     180           1 :                 Long: `
     181           1 : Excise a key range, removing all SSTs inside the range and virtualizing any SSTs
     182           1 : that partially overlap the range.
     183           1 : `,
     184           1 :                 Args: cobra.ExactArgs(1),
     185           1 :                 Run:  d.runExcise,
     186           1 :         }
     187           1 :         d.IOBench = &cobra.Command{
     188           1 :                 Use:   "io-bench <dir>",
     189           1 :                 Short: "perform sstable IO benchmark",
     190           1 :                 Long: `
     191           1 : Run a random IO workload with various IO sizes against the sstables in the
     192           1 : specified database.
     193           1 : `,
     194           1 :                 Args: cobra.ExactArgs(1),
     195           1 :                 Run:  d.runIOBench,
     196           1 :         }
     197           1 : 
     198           1 :         d.Root.AddCommand(d.Check, d.Checkpoint, d.Get, d.Logs, d.LSM, d.Properties, d.Scan, d.Set, d.Space, d.Excise, d.IOBench)
     199           1 :         d.Root.PersistentFlags().BoolVarP(&d.verbose, "verbose", "v", false, "verbose output")
     200           1 : 
     201           1 :         for _, cmd := range []*cobra.Command{d.Check, d.Checkpoint, d.Get, d.LSM, d.Properties, d.Scan, d.Set, d.Space, d.Excise} {
     202           1 :                 cmd.Flags().StringVar(
     203           1 :                         &d.comparerName, "comparer", "", "comparer name (use default if empty)")
     204           1 :                 cmd.Flags().StringVar(
     205           1 :                         &d.mergerName, "merger", "", "merger name (use default if empty)")
     206           1 :         }
     207             : 
     208           1 :         for _, cmd := range []*cobra.Command{d.Scan, d.Get} {
     209           1 :                 cmd.Flags().Var(
     210           1 :                         &d.fmtValue, "value", "value formatter")
     211           1 :         }
     212             : 
     213           1 :         d.Space.Flags().Var(
     214           1 :                 &d.start, "start", "start key for the range")
     215           1 :         d.Space.Flags().Var(
     216           1 :                 &d.end, "end", "inclusive end key for the range")
     217           1 : 
     218           1 :         d.Scan.Flags().Var(
     219           1 :                 &d.fmtKey, "key", "key formatter")
     220           1 :         d.Scan.Flags().Var(
     221           1 :                 &d.start, "start", "start key for the range")
     222           1 :         d.Scan.Flags().Var(
     223           1 :                 &d.end, "end", "exclusive end key for the range")
     224           1 :         d.Scan.Flags().Int64Var(
     225           1 :                 &d.count, "count", 0, "key count for scan (0 is unlimited)")
     226           1 : 
     227           1 :         d.Excise.Flags().Var(
     228           1 :                 &d.start, "start", "start key for the excised range")
     229           1 :         d.Excise.Flags().Var(
     230           1 :                 &d.end, "end", "exclusive end key for the excised range")
     231           1 :         d.Excise.Flags().BoolVar(
     232           1 :                 &d.bypassPrompt, "yes", false, "bypass prompt")
     233           1 : 
     234           1 :         d.IOBench.Flags().BoolVar(
     235           1 :                 &d.allLevels, "all-levels", false, "if set, benchmark all levels (default is only L5/L6)")
     236           1 :         d.IOBench.Flags().IntVar(
     237           1 :                 &d.ioCount, "io-count", 10000, "number of IOs (per IO size) to benchmark")
     238           1 :         d.IOBench.Flags().IntVar(
     239           1 :                 &d.ioParallelism, "io-parallelism", 16, "number of goroutines issuing IO")
     240           1 :         d.IOBench.Flags().StringVar(
     241           1 :                 &d.ioSizes, "io-sizes-kb", "4,16,64,128,256,512,1024", "comma separated list of IO sizes in KB")
     242           1 : 
     243           1 :         return d
     244             : }
     245             : 
     246           1 : func (d *dbT) loadOptions(dir string) error {
     247           1 :         ls, err := d.opts.FS.List(dir)
     248           1 :         if err != nil || len(ls) == 0 {
     249           1 :                 // NB: We don't return the error here as we prefer to return the error from
     250           1 :                 // pebble.Open. Another way to put this is that a non-existent directory is
     251           1 :                 // not a failure in loading the options.
     252           1 :                 return nil
     253           1 :         }
     254             : 
     255           1 :         hooks := &pebble.ParseHooks{
     256           1 :                 NewComparer: func(name string) (*pebble.Comparer, error) {
     257           1 :                         if c := d.comparers[name]; c != nil {
     258           1 :                                 return c, nil
     259           1 :                         }
     260           0 :                         return nil, errors.Errorf("unknown comparer %q", errors.Safe(name))
     261             :                 },
     262           1 :                 NewMerger: func(name string) (*pebble.Merger, error) {
     263           1 :                         if m := d.mergers[name]; m != nil {
     264           1 :                                 return m, nil
     265           1 :                         }
     266           0 :                         return nil, errors.Errorf("unknown merger %q", errors.Safe(name))
     267             :                 },
     268           0 :                 SkipUnknown: func(name, value string) bool {
     269           0 :                         return true
     270           0 :                 },
     271             :         }
     272             : 
     273             :         // TODO(peter): RocksDB sometimes leaves multiple OPTIONS files in
     274             :         // existence. We parse all of them as the comparer and merger shouldn't be
     275             :         // changing. We could parse only the first or the latest. Not clear if this
     276             :         // matters.
     277           1 :         var dbOpts pebble.Options
     278           1 :         for _, filename := range ls {
     279           1 :                 ft, _, ok := base.ParseFilename(d.opts.FS, filename)
     280           1 :                 if !ok {
     281           1 :                         continue
     282             :                 }
     283           1 :                 switch ft {
     284           1 :                 case base.FileTypeOptions:
     285           1 :                         err := func() error {
     286           1 :                                 f, err := d.opts.FS.Open(d.opts.FS.PathJoin(dir, filename))
     287           1 :                                 if err != nil {
     288           0 :                                         return err
     289           0 :                                 }
     290           1 :                                 defer f.Close()
     291           1 : 
     292           1 :                                 data, err := io.ReadAll(f)
     293           1 :                                 if err != nil {
     294           0 :                                         return err
     295           0 :                                 }
     296             : 
     297           1 :                                 if err := dbOpts.Parse(string(data), hooks); err != nil {
     298           1 :                                         return err
     299           1 :                                 }
     300           1 :                                 return nil
     301             :                         }()
     302           1 :                         if err != nil {
     303           1 :                                 return err
     304           1 :                         }
     305             :                 }
     306             :         }
     307             : 
     308           1 :         if dbOpts.Comparer != nil {
     309           1 :                 d.opts.Comparer = dbOpts.Comparer
     310           1 :         }
     311           1 :         if dbOpts.Merger != nil {
     312           1 :                 d.opts.Merger = dbOpts.Merger
     313           1 :         }
     314           1 :         return nil
     315             : }
     316             : 
     317             : // OpenOption is an option that may be applied to the *pebble.Options before
     318             : // calling pebble.Open.
     319             : type OpenOption interface {
     320             :         Apply(dirname string, opts *pebble.Options)
     321             : }
     322             : 
     323           1 : func (d *dbT) openDB(dir string, openOptions ...OpenOption) (*pebble.DB, error) {
     324           1 :         db, err := d.openDBInternal(dir, openOptions...)
     325           1 :         if err != nil {
     326           1 :                 if d.openErrEnhancer != nil {
     327           1 :                         err = d.openErrEnhancer(err)
     328           1 :                 }
     329           1 :                 return nil, err
     330             :         }
     331           1 :         return db, nil
     332             : }
     333             : 
     334           1 : func (d *dbT) openDBInternal(dir string, openOptions ...OpenOption) (*pebble.DB, error) {
     335           1 :         if err := d.loadOptions(dir); err != nil {
     336           1 :                 return nil, errors.Wrap(err, "error loading options")
     337           1 :         }
     338           1 :         if d.comparerName != "" {
     339           1 :                 d.opts.Comparer = d.comparers[d.comparerName]
     340           1 :                 if d.opts.Comparer == nil {
     341           1 :                         return nil, errors.Errorf("unknown comparer %q", errors.Safe(d.comparerName))
     342           1 :                 }
     343             :         }
     344           1 :         if d.mergerName != "" {
     345           1 :                 d.opts.Merger = d.mergers[d.mergerName]
     346           1 :                 if d.opts.Merger == nil {
     347           1 :                         return nil, errors.Errorf("unknown merger %q", errors.Safe(d.mergerName))
     348           1 :                 }
     349             :         }
     350           1 :         opts := *d.opts
     351           1 :         for _, opt := range openOptions {
     352           1 :                 opt.Apply(dir, &opts)
     353           1 :         }
     354           1 :         for _, opt := range d.openOptions {
     355           0 :                 opt.Apply(dir, &opts)
     356           0 :         }
     357           1 :         opts.Cache = pebble.NewCache(128 << 20 /* 128 MB */)
     358           1 :         defer opts.Cache.Unref()
     359           1 :         return pebble.Open(dir, &opts)
     360             : }
     361             : 
     362           1 : func (d *dbT) closeDB(stderr io.Writer, db *pebble.DB) {
     363           1 :         if err := db.Close(); err != nil {
     364           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     365           0 :         }
     366             : }
     367             : 
     368           1 : func (d *dbT) runCheck(cmd *cobra.Command, args []string) {
     369           1 :         stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr()
     370           1 :         db, err := d.openDB(args[0])
     371           1 :         if err != nil {
     372           1 :                 fmt.Fprintf(stderr, "%s\n", err)
     373           1 :                 return
     374           1 :         }
     375           1 :         defer d.closeDB(stderr, db)
     376           1 : 
     377           1 :         var stats pebble.CheckLevelsStats
     378           1 :         if err := db.CheckLevels(&stats); err != nil {
     379           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     380           0 :         }
     381           1 :         fmt.Fprintf(stdout, "checked %d %s and %d %s\n",
     382           1 :                 stats.NumPoints, makePlural("point", stats.NumPoints), stats.NumTombstones, makePlural("tombstone", int64(stats.NumTombstones)))
     383             : }
     384             : 
     385             : type nonReadOnly struct{}
     386             : 
     387           1 : func (n nonReadOnly) Apply(dirname string, opts *pebble.Options) {
     388           1 :         opts.ReadOnly = false
     389           1 :         // Increase the L0 compaction threshold to reduce the likelihood of an
     390           1 :         // unintended compaction changing test output.
     391           1 :         opts.L0CompactionThreshold = 10
     392           1 : }
     393             : 
     394           1 : func (d *dbT) runCheckpoint(cmd *cobra.Command, args []string) {
     395           1 :         stderr := cmd.ErrOrStderr()
     396           1 :         db, err := d.openDB(args[0], nonReadOnly{})
     397           1 :         if err != nil {
     398           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     399           0 :                 return
     400           0 :         }
     401           1 :         defer d.closeDB(stderr, db)
     402           1 :         destDir := args[1]
     403           1 : 
     404           1 :         if err := db.Checkpoint(destDir); err != nil {
     405           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     406           0 :         }
     407             : }
     408             : 
     409           1 : func (d *dbT) runGet(cmd *cobra.Command, args []string) {
     410           1 :         stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr()
     411           1 :         db, err := d.openDB(args[0])
     412           1 :         if err != nil {
     413           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     414           0 :                 return
     415           0 :         }
     416           1 :         defer d.closeDB(stderr, db)
     417           1 :         var k key
     418           1 :         if err := k.Set(args[1]); err != nil {
     419           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     420           0 :                 return
     421           0 :         }
     422             : 
     423           1 :         val, closer, err := db.Get(k)
     424           1 :         if err != nil {
     425           1 :                 fmt.Fprintf(stderr, "%s\n", err)
     426           1 :                 return
     427           1 :         }
     428           1 :         defer func() {
     429           1 :                 if closer != nil {
     430           1 :                         closer.Close()
     431           1 :                 }
     432             :         }()
     433           1 :         if val != nil {
     434           1 :                 fmt.Fprintf(stdout, "%s\n", d.fmtValue.fn(k, val))
     435           1 :         }
     436             : }
     437             : 
     438           1 : func (d *dbT) runLSM(cmd *cobra.Command, args []string) {
     439           1 :         stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr()
     440           1 :         db, err := d.openDB(args[0])
     441           1 :         if err != nil {
     442           1 :                 fmt.Fprintf(stderr, "%s\n", err)
     443           1 :                 return
     444           1 :         }
     445           1 :         defer d.closeDB(stderr, db)
     446           1 : 
     447           1 :         fmt.Fprintf(stdout, "%s", db.Metrics())
     448             : }
     449             : 
     450           1 : func (d *dbT) runScan(cmd *cobra.Command, args []string) {
     451           1 :         stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr()
     452           1 :         db, err := d.openDB(args[0])
     453           1 :         if err != nil {
     454           1 :                 fmt.Fprintf(stderr, "%s\n", err)
     455           1 :                 return
     456           1 :         }
     457           1 :         defer d.closeDB(stderr, db)
     458           1 : 
     459           1 :         // Update the internal formatter if this comparator has one specified.
     460           1 :         if d.opts.Comparer != nil {
     461           1 :                 d.fmtKey.setForComparer(d.opts.Comparer.Name, d.comparers)
     462           1 :                 d.fmtValue.setForComparer(d.opts.Comparer.Name, d.comparers)
     463           1 :         }
     464             : 
     465           1 :         start := timeNow()
     466           1 :         fmtKeys := d.fmtKey.spec != "null"
     467           1 :         fmtValues := d.fmtValue.spec != "null"
     468           1 :         var count int64
     469           1 : 
     470           1 :         iter, _ := db.NewIter(&pebble.IterOptions{
     471           1 :                 UpperBound: d.end,
     472           1 :         })
     473           1 :         for valid := iter.SeekGE(d.start); valid; valid = iter.Next() {
     474           1 :                 if fmtKeys || fmtValues {
     475           1 :                         needDelimiter := false
     476           1 :                         if fmtKeys {
     477           1 :                                 fmt.Fprintf(stdout, "%s", d.fmtKey.fn(iter.Key()))
     478           1 :                                 needDelimiter = true
     479           1 :                         }
     480           1 :                         if fmtValues {
     481           1 :                                 if needDelimiter {
     482           1 :                                         stdout.Write([]byte{' '})
     483           1 :                                 }
     484           1 :                                 fmt.Fprintf(stdout, "%s", d.fmtValue.fn(iter.Key(), iter.Value()))
     485             :                         }
     486           1 :                         stdout.Write([]byte{'\n'})
     487             :                 }
     488             : 
     489           1 :                 count++
     490           1 :                 if d.count > 0 && count >= d.count {
     491           1 :                         break
     492             :                 }
     493             :         }
     494             : 
     495           1 :         if err := iter.Close(); err != nil {
     496           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     497           0 :         }
     498             : 
     499           1 :         elapsed := timeNow().Sub(start)
     500           1 : 
     501           1 :         fmt.Fprintf(stdout, "scanned %d %s in %0.1fs\n",
     502           1 :                 count, makePlural("record", count), elapsed.Seconds())
     503             : }
     504             : 
     505           1 : func (d *dbT) runSpace(cmd *cobra.Command, args []string) {
     506           1 :         stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr()
     507           1 :         db, err := d.openDB(args[0])
     508           1 :         if err != nil {
     509           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     510           0 :                 return
     511           0 :         }
     512           1 :         defer d.closeDB(stdout, db)
     513           1 : 
     514           1 :         bytes, err := db.EstimateDiskUsage(d.start, d.end)
     515           1 :         if err != nil {
     516           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     517           0 :                 return
     518           0 :         }
     519           1 :         fmt.Fprintf(stdout, "%d\n", bytes)
     520             : }
     521             : 
     522           1 : func (d *dbT) getExciseSpan() (pebble.KeyRange, error) {
     523           1 :         // If a DBExciseSpanFn is specified, try to use it and see if it returns a
     524           1 :         // valid span.
     525           1 :         if d.exciseSpanFn != nil {
     526           0 :                 span, err := d.exciseSpanFn()
     527           0 :                 if err != nil {
     528           0 :                         return pebble.KeyRange{}, err
     529           0 :                 }
     530           0 :                 if span.Valid() {
     531           0 :                         if d.start != nil || d.end != nil {
     532           0 :                                 return pebble.KeyRange{}, errors.Errorf(
     533           0 :                                         "--start/--end cannot be used when span is specified by other methods.")
     534           0 :                         }
     535           0 :                         return span, nil
     536             :                 }
     537             :         }
     538           1 :         if d.start == nil || d.end == nil {
     539           1 :                 return pebble.KeyRange{}, errors.Errorf("excise range not specified.")
     540           1 :         }
     541           1 :         return pebble.KeyRange{
     542           1 :                 Start: d.start,
     543           1 :                 End:   d.end,
     544           1 :         }, nil
     545             : }
     546             : 
     547           1 : func (d *dbT) runExcise(cmd *cobra.Command, args []string) {
     548           1 :         stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr()
     549           1 : 
     550           1 :         span, err := d.getExciseSpan()
     551           1 :         if err != nil {
     552           1 :                 fmt.Fprintf(stderr, "Error: %v\n", err)
     553           1 :                 return
     554           1 :         }
     555             : 
     556           1 :         dbOpts := d.opts.EnsureDefaults()
     557           1 :         // Disable all processes that would try to open tables: table stats,
     558           1 :         // consistency check, automatic compactions.
     559           1 :         dbOpts.DisableTableStats = true
     560           1 :         dbOpts.DisableConsistencyCheck = true
     561           1 :         dbOpts.DisableAutomaticCompactions = true
     562           1 : 
     563           1 :         dbDir := args[0]
     564           1 :         db, err := d.openDB(dbDir, nonReadOnly{})
     565           1 :         if err != nil {
     566           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     567           0 :                 return
     568           0 :         }
     569           1 :         defer d.closeDB(stdout, db)
     570           1 : 
     571           1 :         // Update the internal formatter if this comparator has one specified.
     572           1 :         if d.opts.Comparer != nil {
     573           1 :                 d.fmtKey.setForComparer(d.opts.Comparer.Name, d.comparers)
     574           1 :         }
     575             : 
     576           1 :         fmt.Fprintf(stdout, "Excising range:\n")
     577           1 :         fmt.Fprintf(stdout, "  start: %s\n", d.fmtKey.fn(span.Start))
     578           1 :         fmt.Fprintf(stdout, "  end:   %s\n", d.fmtKey.fn(span.End))
     579           1 : 
     580           1 :         if !d.bypassPrompt {
     581           0 :                 fmt.Fprintf(stdout, "WARNING!!!\n")
     582           0 :                 fmt.Fprintf(stdout, "This command will remove all keys in this range!\n")
     583           0 :                 reader := bufio.NewReader(cmd.InOrStdin())
     584           0 :                 for {
     585           0 :                         fmt.Fprintf(stdout, "Continue? [Y/N] ")
     586           0 :                         answer, _ := reader.ReadString('\n')
     587           0 :                         answer = strings.ToLower(strings.TrimSpace(answer))
     588           0 :                         if answer == "y" || answer == "yes" {
     589           0 :                                 break
     590             :                         }
     591             : 
     592           0 :                         if answer == "n" || answer == "no" {
     593           0 :                                 fmt.Fprintf(stderr, "Aborting\n")
     594           0 :                                 return
     595           0 :                         }
     596             :                 }
     597             :         }
     598             : 
     599             :         // Write a temporary sst that only has excise tombstones. We write it inside
     600             :         // the database directory so that the command works against any FS.
     601             :         // TODO(radu): remove this if we add a separate DB.Excise method.
     602           1 :         path := dbOpts.FS.PathJoin(dbDir, fmt.Sprintf("excise-%0x.sst", rand.Uint32()))
     603           1 :         defer dbOpts.FS.Remove(path)
     604           1 :         f, err := dbOpts.FS.Create(path, vfs.WriteCategoryUnspecified)
     605           1 :         if err != nil {
     606           0 :                 fmt.Fprintf(stderr, "Error creating temporary sst file %q: %s\n", path, err)
     607           0 :                 return
     608           0 :         }
     609           1 :         writable := objstorageprovider.NewFileWritable(f)
     610           1 :         writerOpts := dbOpts.MakeWriterOptions(0, db.FormatMajorVersion().MaxTableFormat())
     611           1 :         w := sstable.NewWriter(writable, writerOpts)
     612           1 :         err = w.DeleteRange(span.Start, span.End)
     613           1 :         err = errors.CombineErrors(err, w.RangeKeyDelete(span.Start, span.End))
     614           1 :         err = errors.CombineErrors(err, w.Close())
     615           1 :         if err != nil {
     616           0 :                 fmt.Fprintf(stderr, "Error writing temporary sst file %q: %s\n", path, err)
     617           0 :                 return
     618           0 :         }
     619             : 
     620           1 :         _, err = db.IngestAndExcise([]string{path}, nil, nil, span, true /* sstsContainExciseTombstone */)
     621           1 :         if err != nil {
     622           0 :                 fmt.Fprintf(stderr, "Error excising: %s\n", err)
     623           0 :                 return
     624           0 :         }
     625           1 :         fmt.Fprintf(stdout, "Excise complete.\n")
     626             : }
     627             : 
     628           1 : func (d *dbT) runProperties(cmd *cobra.Command, args []string) {
     629           1 :         stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr()
     630           1 :         dirname := args[0]
     631           1 :         err := func() error {
     632           1 :                 desc, err := pebble.Peek(dirname, d.opts.FS)
     633           1 :                 if err != nil {
     634           1 :                         return err
     635           1 :                 } else if !desc.Exists {
     636           0 :                         return oserror.ErrNotExist
     637           0 :                 }
     638           1 :                 manifestFilename := d.opts.FS.PathBase(desc.ManifestFilename)
     639           1 : 
     640           1 :                 // Replay the manifest to get the current version.
     641           1 :                 f, err := d.opts.FS.Open(desc.ManifestFilename)
     642           1 :                 if err != nil {
     643           0 :                         return errors.Wrapf(err, "pebble: could not open MANIFEST file %q", manifestFilename)
     644           0 :                 }
     645           1 :                 defer f.Close()
     646           1 : 
     647           1 :                 cmp := base.DefaultComparer
     648           1 :                 var bve manifest.BulkVersionEdit
     649           1 :                 bve.AddedByFileNum = make(map[base.FileNum]*manifest.FileMetadata)
     650           1 :                 rr := record.NewReader(f, 0 /* logNum */)
     651           1 :                 for {
     652           1 :                         r, err := rr.Next()
     653           1 :                         if err == io.EOF {
     654           1 :                                 break
     655             :                         }
     656           1 :                         if err != nil {
     657           0 :                                 return errors.Wrapf(err, "pebble: reading manifest %q", manifestFilename)
     658           0 :                         }
     659           1 :                         var ve manifest.VersionEdit
     660           1 :                         err = ve.Decode(r)
     661           1 :                         if err != nil {
     662           0 :                                 return err
     663           0 :                         }
     664           1 :                         if err := bve.Accumulate(&ve); err != nil {
     665           0 :                                 return err
     666           0 :                         }
     667           1 :                         if ve.ComparerName != "" {
     668           1 :                                 cmp = d.comparers[ve.ComparerName]
     669           1 :                                 d.fmtKey.setForComparer(ve.ComparerName, d.comparers)
     670           1 :                                 d.fmtValue.setForComparer(ve.ComparerName, d.comparers)
     671           1 :                         }
     672             :                 }
     673           1 :                 v, err := bve.Apply(
     674           1 :                         nil /* version */, cmp, d.opts.FlushSplitBytes,
     675           1 :                         d.opts.Experimental.ReadCompactionRate,
     676           1 :                 )
     677           1 :                 if err != nil {
     678           0 :                         return err
     679           0 :                 }
     680             : 
     681           1 :                 objProvider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(d.opts.FS, dirname))
     682           1 :                 if err != nil {
     683           0 :                         return err
     684           0 :                 }
     685           1 :                 defer objProvider.Close()
     686           1 : 
     687           1 :                 // Load and aggregate sstable properties.
     688           1 :                 tw := tabwriter.NewWriter(stdout, 2, 1, 4, ' ', 0)
     689           1 :                 var total props
     690           1 :                 var all []props
     691           1 :                 for _, l := range v.Levels {
     692           1 :                         iter := l.Iter()
     693           1 :                         var level props
     694           1 :                         for t := iter.First(); t != nil; t = iter.Next() {
     695           1 :                                 if t.Virtual {
     696           0 :                                         // TODO(bananabrick): Handle virtual sstables here. We don't
     697           0 :                                         // really have any stats or properties at this point. Maybe
     698           0 :                                         // we could approximate some of these properties for virtual
     699           0 :                                         // sstables by first grabbing properties for the backing
     700           0 :                                         // physical sstable, and then extrapolating.
     701           0 :                                         continue
     702             :                                 }
     703           1 :                                 err := d.addProps(objProvider, t.PhysicalMeta(), &level)
     704           1 :                                 if err != nil {
     705           0 :                                         return err
     706           0 :                                 }
     707             :                         }
     708           1 :                         all = append(all, level)
     709           1 :                         total.update(level)
     710             :                 }
     711           1 :                 all = append(all, total)
     712           1 : 
     713           1 :                 fmt.Fprintln(tw, "\tL0\tL1\tL2\tL3\tL4\tL5\tL6\tTOTAL")
     714           1 : 
     715           1 :                 fmt.Fprintf(tw, "count\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n",
     716           1 :                         propArgs(all, func(p *props) interface{} { return p.Count })...)
     717             : 
     718           1 :                 fmt.Fprintln(tw, "seq num\t\t\t\t\t\t\t\t")
     719           1 :                 fmt.Fprintf(tw, "  smallest\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n",
     720           1 :                         propArgs(all, func(p *props) interface{} { return p.SmallestSeqNum })...)
     721           1 :                 fmt.Fprintf(tw, "  largest\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n",
     722           1 :                         propArgs(all, func(p *props) interface{} { return p.LargestSeqNum })...)
     723             : 
     724           1 :                 fmt.Fprintln(tw, "size\t\t\t\t\t\t\t\t")
     725           1 :                 fmt.Fprintf(tw, "  data\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     726           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.DataSize) })...)
     727           1 :                 fmt.Fprintf(tw, "    blocks\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n",
     728           1 :                         propArgs(all, func(p *props) interface{} { return p.NumDataBlocks })...)
     729           1 :                 fmt.Fprintf(tw, "  index\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     730           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.IndexSize) })...)
     731           1 :                 fmt.Fprintf(tw, "    blocks\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n",
     732           1 :                         propArgs(all, func(p *props) interface{} { return p.NumIndexBlocks })...)
     733           1 :                 fmt.Fprintf(tw, "    top-level\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     734           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.TopLevelIndexSize) })...)
     735           1 :                 fmt.Fprintf(tw, "  filter\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     736           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.FilterSize) })...)
     737           1 :                 fmt.Fprintf(tw, "  raw-key\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     738           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.RawKeySize) })...)
     739           1 :                 fmt.Fprintf(tw, "  raw-value\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     740           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.RawValueSize) })...)
     741           1 :                 fmt.Fprintf(tw, "  pinned-key\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     742           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.SnapshotPinnedKeySize) })...)
     743           1 :                 fmt.Fprintf(tw, "  pinned-value\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     744           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.SnapshotPinnedValueSize) })...)
     745           1 :                 fmt.Fprintf(tw, "  point-del-key-size\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     746           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.RawPointTombstoneKeySize) })...)
     747           1 :                 fmt.Fprintf(tw, "  point-del-value-size\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     748           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Bytes.Uint64(p.RawPointTombstoneValueSize) })...)
     749             : 
     750           1 :                 fmt.Fprintln(tw, "records\t\t\t\t\t\t\t\t")
     751           1 :                 fmt.Fprintf(tw, "  set\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     752           1 :                         propArgs(all, func(p *props) interface{} {
     753           1 :                                 return humanize.Count.Uint64(p.NumEntries - p.NumDeletions - p.NumMergeOperands)
     754           1 :                         })...)
     755           1 :                 fmt.Fprintf(tw, "  delete\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     756           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumDeletions - p.NumRangeDeletions) })...)
     757           1 :                 fmt.Fprintf(tw, "  delete-sized\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     758           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumSizedDeletions) })...)
     759           1 :                 fmt.Fprintf(tw, "  range-delete\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     760           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumRangeDeletions) })...)
     761           1 :                 fmt.Fprintf(tw, "  range-key-sets\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     762           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumRangeKeySets) })...)
     763           1 :                 fmt.Fprintf(tw, "  range-key-unsets\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     764           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumRangeKeyUnSets) })...)
     765           1 :                 fmt.Fprintf(tw, "  range-key-deletes\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     766           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumRangeKeyDeletes) })...)
     767           1 :                 fmt.Fprintf(tw, "  merge\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     768           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.NumMergeOperands) })...)
     769           1 :                 fmt.Fprintf(tw, "  pinned\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
     770           1 :                         propArgs(all, func(p *props) interface{} { return humanize.Count.Uint64(p.SnapshotPinnedKeys) })...)
     771             : 
     772           1 :                 if err := tw.Flush(); err != nil {
     773           0 :                         return err
     774           0 :                 }
     775           1 :                 return nil
     776             :         }()
     777           1 :         if err != nil {
     778           1 :                 fmt.Fprintln(stderr, err)
     779           1 :         }
     780             : }
     781             : 
     782           1 : func (d *dbT) runSet(cmd *cobra.Command, args []string) {
     783           1 :         stderr := cmd.ErrOrStderr()
     784           1 :         db, err := d.openDB(args[0], nonReadOnly{})
     785           1 :         if err != nil {
     786           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     787           0 :                 return
     788           0 :         }
     789           1 :         defer d.closeDB(stderr, db)
     790           1 :         var k, v key
     791           1 :         if err := k.Set(args[1]); err != nil {
     792           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     793           0 :                 return
     794           0 :         }
     795           1 :         if err := v.Set(args[2]); err != nil {
     796           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     797           0 :                 return
     798           0 :         }
     799             : 
     800           1 :         if err := db.Set(k, v, nil); err != nil {
     801           0 :                 fmt.Fprintf(stderr, "%s\n", err)
     802           0 :         }
     803             : }
     804             : 
     805           1 : func propArgs(props []props, getProp func(*props) interface{}) []interface{} {
     806           1 :         args := make([]interface{}, 0, len(props))
     807           1 :         for _, p := range props {
     808           1 :                 args = append(args, getProp(&p))
     809           1 :         }
     810           1 :         return args
     811             : }
     812             : 
     813             : type props struct {
     814             :         Count                      uint64
     815             :         SmallestSeqNum             uint64
     816             :         LargestSeqNum              uint64
     817             :         DataSize                   uint64
     818             :         FilterSize                 uint64
     819             :         IndexSize                  uint64
     820             :         NumDataBlocks              uint64
     821             :         NumIndexBlocks             uint64
     822             :         NumDeletions               uint64
     823             :         NumSizedDeletions          uint64
     824             :         NumEntries                 uint64
     825             :         NumMergeOperands           uint64
     826             :         NumRangeDeletions          uint64
     827             :         NumRangeKeySets            uint64
     828             :         NumRangeKeyUnSets          uint64
     829             :         NumRangeKeyDeletes         uint64
     830             :         RawKeySize                 uint64
     831             :         RawPointTombstoneKeySize   uint64
     832             :         RawPointTombstoneValueSize uint64
     833             :         RawValueSize               uint64
     834             :         SnapshotPinnedKeys         uint64
     835             :         SnapshotPinnedKeySize      uint64
     836             :         SnapshotPinnedValueSize    uint64
     837             :         TopLevelIndexSize          uint64
     838             : }
     839             : 
     840           1 : func (p *props) update(o props) {
     841           1 :         p.Count += o.Count
     842           1 :         if o.SmallestSeqNum != 0 && (o.SmallestSeqNum < p.SmallestSeqNum || p.SmallestSeqNum == 0) {
     843           1 :                 p.SmallestSeqNum = o.SmallestSeqNum
     844           1 :         }
     845           1 :         if o.LargestSeqNum > p.LargestSeqNum {
     846           1 :                 p.LargestSeqNum = o.LargestSeqNum
     847           1 :         }
     848           1 :         p.DataSize += o.DataSize
     849           1 :         p.FilterSize += o.FilterSize
     850           1 :         p.IndexSize += o.IndexSize
     851           1 :         p.NumDataBlocks += o.NumDataBlocks
     852           1 :         p.NumIndexBlocks += o.NumIndexBlocks
     853           1 :         p.NumDeletions += o.NumDeletions
     854           1 :         p.NumSizedDeletions += o.NumSizedDeletions
     855           1 :         p.NumEntries += o.NumEntries
     856           1 :         p.NumMergeOperands += o.NumMergeOperands
     857           1 :         p.NumRangeDeletions += o.NumRangeDeletions
     858           1 :         p.NumRangeKeySets += o.NumRangeKeySets
     859           1 :         p.NumRangeKeyUnSets += o.NumRangeKeyUnSets
     860           1 :         p.NumRangeKeyDeletes += o.NumRangeKeyDeletes
     861           1 :         p.RawKeySize += o.RawKeySize
     862           1 :         p.RawPointTombstoneKeySize += o.RawPointTombstoneKeySize
     863           1 :         p.RawPointTombstoneValueSize += o.RawPointTombstoneValueSize
     864           1 :         p.RawValueSize += o.RawValueSize
     865           1 :         p.SnapshotPinnedKeySize += o.SnapshotPinnedKeySize
     866           1 :         p.SnapshotPinnedValueSize += o.SnapshotPinnedValueSize
     867           1 :         p.SnapshotPinnedKeys += o.SnapshotPinnedKeys
     868           1 :         p.TopLevelIndexSize += o.TopLevelIndexSize
     869             : }
     870             : 
     871             : func (d *dbT) addProps(
     872             :         objProvider objstorage.Provider, m manifest.PhysicalFileMeta, p *props,
     873           1 : ) error {
     874           1 :         ctx := context.Background()
     875           1 :         f, err := objProvider.OpenForReading(ctx, base.FileTypeTable, m.FileBacking.DiskFileNum, objstorage.OpenOptions{})
     876           1 :         if err != nil {
     877           0 :                 return err
     878           0 :         }
     879           1 :         r, err := sstable.NewReader(f, sstable.ReaderOptions{}, d.mergers, d.comparers)
     880           1 :         if err != nil {
     881           0 :                 _ = f.Close()
     882           0 :                 return err
     883           0 :         }
     884           1 :         p.update(props{
     885           1 :                 Count:                      1,
     886           1 :                 SmallestSeqNum:             m.SmallestSeqNum,
     887           1 :                 LargestSeqNum:              m.LargestSeqNum,
     888           1 :                 DataSize:                   r.Properties.DataSize,
     889           1 :                 FilterSize:                 r.Properties.FilterSize,
     890           1 :                 IndexSize:                  r.Properties.IndexSize,
     891           1 :                 NumDataBlocks:              r.Properties.NumDataBlocks,
     892           1 :                 NumIndexBlocks:             1 + r.Properties.IndexPartitions,
     893           1 :                 NumDeletions:               r.Properties.NumDeletions,
     894           1 :                 NumSizedDeletions:          r.Properties.NumSizedDeletions,
     895           1 :                 NumEntries:                 r.Properties.NumEntries,
     896           1 :                 NumMergeOperands:           r.Properties.NumMergeOperands,
     897           1 :                 NumRangeDeletions:          r.Properties.NumRangeDeletions,
     898           1 :                 NumRangeKeySets:            r.Properties.NumRangeKeySets,
     899           1 :                 NumRangeKeyUnSets:          r.Properties.NumRangeKeyUnsets,
     900           1 :                 NumRangeKeyDeletes:         r.Properties.NumRangeKeyDels,
     901           1 :                 RawKeySize:                 r.Properties.RawKeySize,
     902           1 :                 RawPointTombstoneKeySize:   r.Properties.RawPointTombstoneKeySize,
     903           1 :                 RawPointTombstoneValueSize: r.Properties.RawPointTombstoneValueSize,
     904           1 :                 RawValueSize:               r.Properties.RawValueSize,
     905           1 :                 SnapshotPinnedKeySize:      r.Properties.SnapshotPinnedKeySize,
     906           1 :                 SnapshotPinnedValueSize:    r.Properties.SnapshotPinnedValueSize,
     907           1 :                 SnapshotPinnedKeys:         r.Properties.SnapshotPinnedKeys,
     908           1 :                 TopLevelIndexSize:          r.Properties.TopLevelIndexSize,
     909           1 :         })
     910           1 :         return r.Close()
     911             : }
     912             : 
     913           1 : func makePlural(singular string, count int64) string {
     914           1 :         if count > 1 {
     915           1 :                 return fmt.Sprintf("%ss", singular)
     916           1 :         }
     917           1 :         return singular
     918             : }

Generated by: LCOV version 1.14