• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

freeeve / tinykvs / 21198750949

21 Jan 2026 05:42AM UTC coverage: 81.104% (+7.8%) from 73.271%
21198750949

push

github

freeeve
feat: add minlz compression and refactor CLI for testability

- Add minlz as compression option with 3 levels (fastest/balanced/smallest)
- Make minlz the default compression (3x faster than zstd, similar ratio)
- Refactor CLI into testable struct with injectable dependencies
- Extract helper functions to separate file
- Add comprehensive CLI and helper unit tests
- Improve test coverage from 45.9% to 72.6%

BREAKING: New stores will use minlz by default. Existing zstd/snappy
stores remain fully readable.

743 of 948 new or added lines in 6 files covered. (78.38%)

3 existing lines in 2 files now uncovered.

6670 of 8224 relevant lines covered (81.1%)

400835.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.07
/cmd/tinykvs/cli.go
1
package main
2

3
import (
4
        "bufio"
5
        "encoding/csv"
6
        "encoding/hex"
7
        "flag"
8
        "fmt"
9
        "io"
10
        "os"
11
        "path/filepath"
12
        "strconv"
13

14
        "github.com/freeeve/tinykvs"
15
)
16

17
// CLI holds injectable dependencies for testability.
18
type CLI struct {
19
        Stdout io.Writer
20
        Stderr io.Writer
21
        Getenv func(string) string
22
}
23

24
// NewCLI creates a CLI with default OS dependencies.
25
func NewCLI() *CLI {
1✔
26
        return &CLI{
1✔
27
                Stdout: os.Stdout,
1✔
28
                Stderr: os.Stderr,
1✔
29
                Getenv: os.Getenv,
1✔
30
        }
1✔
31
}
1✔
32

33
// Run executes the CLI and returns an exit code (0 = success, 1 = error).
34
func (c *CLI) Run(args []string) int {
75✔
35
        if len(args) < 2 {
76✔
36
                c.printUsage()
1✔
37
                return 1
1✔
38
        }
1✔
39

40
        cmd := args[1]
74✔
41
        cmdArgs := args[2:]
74✔
42

74✔
43
        switch cmd {
74✔
44
        case "version", "-v", "--version":
4✔
45
                fmt.Fprintln(c.Stdout, "tinykvs "+versionString())
4✔
46
                return 0
4✔
47
        case "get":
23✔
48
                return c.cmdGet(cmdArgs)
23✔
49
        case "put":
13✔
50
                return c.cmdPut(cmdArgs)
13✔
51
        case "delete":
4✔
52
                return c.cmdDelete(cmdArgs)
4✔
53
        case "scan":
5✔
54
                return c.cmdScan(cmdArgs)
5✔
55
        case "stats":
2✔
56
                return c.cmdStats(cmdArgs)
2✔
NEW
57
        case "count":
×
NEW
58
                return c.cmdCount(cmdArgs)
×
59
        case "compact":
2✔
60
                return c.cmdCompact(cmdArgs)
2✔
61
        case "repair":
3✔
62
                return c.cmdRepair(cmdArgs)
3✔
63
        case "info":
5✔
64
                return c.cmdInfo(cmdArgs)
5✔
65
        case "export":
5✔
66
                return c.cmdExport(cmdArgs)
5✔
67
        case "import":
3✔
68
                return c.cmdImport(cmdArgs)
3✔
NEW
69
        case "shell":
×
NEW
70
                return c.cmdShell(cmdArgs)
×
71
        case "help", "-h", "--help":
4✔
72
                c.printUsage()
4✔
73
                return 0
4✔
74
        default:
1✔
75
                fmt.Fprintf(c.Stderr, "Unknown command: %s\n\n", cmd)
1✔
76
                c.printUsage()
1✔
77
                return 1
1✔
78
        }
79
}
80

81
func (c *CLI) printUsage() {
6✔
82
        fmt.Fprintln(c.Stdout, `tinykvs - CLI for TinyKVS stores
6✔
83

6✔
84
Usage:
6✔
85
  tinykvs <command> [options]
6✔
86

6✔
87
Commands:
6✔
88
  get      Get a value by key
6✔
89
  put      Put a key-value pair
6✔
90
  delete   Delete a key
6✔
91
  scan     Scan keys with a prefix
6✔
92
  stats    Show store statistics
6✔
93
  count    Count keys by prefix
6✔
94
  compact  Compact the store
6✔
95
  repair   Remove orphan SST files
6✔
96
  info     Show which level/SSTable contains a key
6✔
97
  export   Export store to CSV file
6✔
98
  import   Import data from CSV file
6✔
99
  shell    Interactive SQL-like query shell
6✔
100

6✔
101
Environment:
6✔
102
  TINYKVS_STORE  Default store directory (used if -dir not specified)
6✔
103

6✔
104
Examples:
6✔
105
  export TINYKVS_STORE=/path/to/store
6✔
106
  tinykvs get -key mykey
6✔
107
  tinykvs put -key mykey -value "hello world"
6✔
108
  tinykvs scan -prefix-hex 05 -limit 10
6✔
109

6✔
110
  # Or specify -dir explicitly:
6✔
111
  tinykvs get -dir /path/to/store -key-hex 0506abcd
6✔
112
  tinykvs stats -dir /path/to/store
6✔
113

6✔
114
Use "tinykvs <command> -h" for more information about a command.`)
6✔
115
}
6✔
116

117
// getDir returns the store directory from flag or TINYKVS_STORE env var.
118
func (c *CLI) getDir(flagDir string) string {
70✔
119
        if flagDir != "" {
135✔
120
                return flagDir
65✔
121
        }
65✔
122
        if envDir := c.Getenv("TINYKVS_STORE"); envDir != "" {
7✔
123
                return envDir
2✔
124
        }
2✔
125
        return ""
3✔
126
}
127

128
// requireDir returns the directory or returns empty string and false if not set.
129
func (c *CLI) requireDir(flagDir string) (string, bool) {
67✔
130
        dir := c.getDir(flagDir)
67✔
131
        if dir == "" {
69✔
132
                fmt.Fprintln(c.Stderr, "Error: -dir is required (or set TINYKVS_STORE)")
2✔
133
                return "", false
2✔
134
        }
2✔
135
        return dir, true
65✔
136
}
137

138
func (c *CLI) cmdGet(args []string) int {
23✔
139
        fs := flag.NewFlagSet("get", flag.ContinueOnError)
23✔
140
        fs.SetOutput(c.Stderr)
23✔
141
        dir := fs.String("dir", "", "Store directory")
23✔
142
        key := fs.String("key", "", "Key to get (string)")
23✔
143
        keyHex := fs.String(flagKeyHex, "", "Key to get (hex encoded)")
23✔
144
        if err := fs.Parse(args); err != nil {
23✔
NEW
145
                return 1
×
NEW
146
        }
×
147

148
        storeDir, ok := c.requireDir(*dir)
23✔
149
        if !ok {
24✔
150
                return 1
1✔
151
        }
1✔
152

153
        var keyBytes []byte
22✔
154
        if *keyHex != "" {
26✔
155
                var err error
4✔
156
                keyBytes, err = hex.DecodeString(*keyHex)
4✔
157
                if err != nil {
5✔
158
                        fmt.Fprintf(c.Stderr, msgErrDecodeHexKey, err)
1✔
159
                        return 1
1✔
160
                }
1✔
161
        } else if *key != "" {
35✔
162
                keyBytes = []byte(*key)
17✔
163
        } else {
18✔
164
                fmt.Fprintln(c.Stderr, msgErrKeyRequired)
1✔
165
                fs.Usage()
1✔
166
                return 1
1✔
167
        }
1✔
168

169
        opts := tinykvs.DefaultOptions(storeDir)
20✔
170
        store, err := tinykvs.Open(storeDir, opts)
20✔
171
        if err != nil {
21✔
172
                fmt.Fprintf(c.Stderr, msgErrOpenStore, err)
1✔
173
                return 1
1✔
174
        }
1✔
175
        defer store.Close()
19✔
176

19✔
177
        val, err := store.Get(keyBytes)
19✔
178
        if err != nil {
22✔
179
                if err == tinykvs.ErrKeyNotFound {
6✔
180
                        fmt.Fprintln(c.Stdout, "Key not found")
3✔
181
                        return 0
3✔
182
                }
3✔
NEW
183
                fmt.Fprintf(c.Stderr, msgErr, err)
×
NEW
184
                return 1
×
185
        }
186

187
        c.printValue(keyBytes, val)
16✔
188
        return 0
16✔
189
}
190

191
func (c *CLI) cmdPut(args []string) int {
13✔
192
        fs := flag.NewFlagSet("put", flag.ContinueOnError)
13✔
193
        fs.SetOutput(c.Stderr)
13✔
194
        dir := fs.String("dir", "", "Store directory")
13✔
195
        pf := &putFlags{args: args}
13✔
196
        fs.StringVar(&pf.key, "key", "", "Key (string)")
13✔
197
        fs.StringVar(&pf.keyHex, flagKeyHex, "", "Key (hex encoded)")
13✔
198
        fs.StringVar(&pf.value, "value", "", "Value (string)")
13✔
199
        fs.StringVar(&pf.valueHex, "value-hex", "", "Value (hex encoded)")
13✔
200
        fs.Int64Var(&pf.valueInt, "value-int", 0, "Value (int64)")
13✔
201
        fs.Float64Var(&pf.valueFloat, "value-float", 0, "Value (float64)")
13✔
202
        fs.StringVar(&pf.valueBool, "value-bool", "", "Value (bool: true/false)")
13✔
203
        fs.BoolVar(&pf.flush, "flush", false, "Flush after put")
13✔
204
        if err := fs.Parse(args); err != nil {
13✔
NEW
205
                return 1
×
NEW
206
        }
×
207

208
        storeDir, ok := c.requireDir(*dir)
13✔
209
        if !ok {
13✔
NEW
210
                return 1
×
NEW
211
        }
×
212

213
        keyBytes, err := parseCLIKey(pf.key, pf.keyHex)
13✔
214
        if err != nil {
13✔
NEW
215
                fmt.Fprintln(c.Stderr, err)
×
NEW
216
                fs.Usage()
×
NEW
217
                return 1
×
NEW
218
        }
×
219

220
        val, err := pf.parseValue()
13✔
221
        if err != nil {
14✔
222
                fmt.Fprintln(c.Stderr, err)
1✔
223
                fs.Usage()
1✔
224
                return 1
1✔
225
        }
1✔
226

227
        opts := tinykvs.DefaultOptions(storeDir)
12✔
228
        store, err := tinykvs.Open(storeDir, opts)
12✔
229
        if err != nil {
13✔
230
                fmt.Fprintf(c.Stderr, msgErrOpenStore, err)
1✔
231
                return 1
1✔
232
        }
1✔
233
        defer store.Close()
11✔
234

11✔
235
        if err := store.Put(keyBytes, val); err != nil {
11✔
NEW
236
                fmt.Fprintf(c.Stderr, msgErr, err)
×
NEW
237
                return 1
×
NEW
238
        }
×
239

240
        if pf.flush {
22✔
241
                if err := store.Flush(); err != nil {
11✔
NEW
242
                        fmt.Fprintf(c.Stderr, "Error flushing: %v\n", err)
×
NEW
243
                        return 1
×
NEW
244
                }
×
245
        }
246

247
        fmt.Fprintln(c.Stdout, "OK")
11✔
248
        return 0
11✔
249
}
250

251
func (c *CLI) cmdDelete(args []string) int {
4✔
252
        fs := flag.NewFlagSet("delete", flag.ContinueOnError)
4✔
253
        fs.SetOutput(c.Stderr)
4✔
254
        dir := fs.String("dir", "", "Store directory")
4✔
255
        key := fs.String("key", "", "Key to delete (string)")
4✔
256
        keyHex := fs.String(flagKeyHex, "", "Key to delete (hex encoded)")
4✔
257
        flush := fs.Bool("flush", false, "Flush after delete")
4✔
258
        if err := fs.Parse(args); err != nil {
4✔
NEW
259
                return 1
×
NEW
260
        }
×
261

262
        storeDir, ok := c.requireDir(*dir)
4✔
263
        if !ok {
4✔
NEW
264
                return 1
×
NEW
265
        }
×
266

267
        var keyBytes []byte
4✔
268
        if *keyHex != "" {
6✔
269
                var err error
2✔
270
                keyBytes, err = hex.DecodeString(*keyHex)
2✔
271
                if err != nil {
3✔
272
                        fmt.Fprintf(c.Stderr, msgErrDecodeHexKey, err)
1✔
273
                        return 1
1✔
274
                }
1✔
275
        } else if *key != "" {
3✔
276
                keyBytes = []byte(*key)
1✔
277
        } else {
2✔
278
                fmt.Fprintln(c.Stderr, msgErrKeyRequired)
1✔
279
                fs.Usage()
1✔
280
                return 1
1✔
281
        }
1✔
282

283
        opts := tinykvs.DefaultOptions(storeDir)
2✔
284
        store, err := tinykvs.Open(storeDir, opts)
2✔
285
        if err != nil {
2✔
NEW
286
                fmt.Fprintf(c.Stderr, msgErrOpenStore, err)
×
NEW
287
                return 1
×
NEW
288
        }
×
289
        defer store.Close()
2✔
290

2✔
291
        if err := store.Delete(keyBytes); err != nil {
2✔
NEW
292
                fmt.Fprintf(c.Stderr, msgErr, err)
×
NEW
293
                return 1
×
NEW
294
        }
×
295

296
        if *flush {
4✔
297
                if err := store.Flush(); err != nil {
2✔
NEW
298
                        fmt.Fprintf(c.Stderr, "Error flushing: %v\n", err)
×
NEW
299
                        return 1
×
NEW
300
                }
×
301
        }
302

303
        fmt.Fprintln(c.Stdout, "OK")
2✔
304
        return 0
2✔
305
}
306

307
func (c *CLI) cmdScan(args []string) int {
5✔
308
        fs := flag.NewFlagSet("scan", flag.ContinueOnError)
5✔
309
        fs.SetOutput(c.Stderr)
5✔
310
        dir := fs.String("dir", "", "Store directory")
5✔
311
        prefix := fs.String("prefix", "", "Key prefix (string)")
5✔
312
        prefixHex := fs.String("prefix-hex", "", "Key prefix (hex encoded)")
5✔
313
        limit := fs.Int("limit", 100, "Maximum number of results")
5✔
314
        keysOnly := fs.Bool("keys-only", false, "Only print keys, not values")
5✔
315
        if err := fs.Parse(args); err != nil {
5✔
NEW
316
                return 1
×
NEW
317
        }
×
318

319
        storeDir, ok := c.requireDir(*dir)
5✔
320
        if !ok {
5✔
NEW
321
                return 1
×
NEW
322
        }
×
323

324
        var prefixBytes []byte
5✔
325
        if *prefixHex != "" {
7✔
326
                var err error
2✔
327
                prefixBytes, err = hex.DecodeString(*prefixHex)
2✔
328
                if err != nil {
3✔
329
                        fmt.Fprintf(c.Stderr, "Error decoding hex prefix: %v\n", err)
1✔
330
                        return 1
1✔
331
                }
1✔
332
        } else if *prefix != "" {
6✔
333
                prefixBytes = []byte(*prefix)
3✔
334
        }
3✔
335

336
        opts := tinykvs.DefaultOptions(storeDir)
4✔
337
        store, err := tinykvs.Open(storeDir, opts)
4✔
338
        if err != nil {
4✔
NEW
339
                fmt.Fprintf(c.Stderr, msgErrOpenStore, err)
×
NEW
340
                return 1
×
NEW
341
        }
×
342
        defer store.Close()
4✔
343

4✔
344
        count := 0
4✔
345
        err = store.ScanPrefix(prefixBytes, func(key []byte, val tinykvs.Value) bool {
13✔
346
                if count >= *limit {
10✔
347
                        return false
1✔
348
                }
1✔
349
                if *keysOnly {
10✔
350
                        fmt.Fprintf(c.Stdout, "%s\n", formatKey(key))
2✔
351
                } else {
8✔
352
                        c.printValue(key, val)
6✔
353
                }
6✔
354
                count++
8✔
355
                return true
8✔
356
        })
357

358
        if err != nil {
4✔
NEW
359
                fmt.Fprintf(c.Stderr, "Error scanning: %v\n", err)
×
NEW
360
                return 1
×
NEW
361
        }
×
362

363
        fmt.Fprintf(c.Stderr, "\n(%d results)\n", count)
4✔
364
        return 0
4✔
365
}
366

367
func (c *CLI) cmdStats(args []string) int {
2✔
368
        fs := flag.NewFlagSet("stats", flag.ContinueOnError)
2✔
369
        fs.SetOutput(c.Stderr)
2✔
370
        dir := fs.String("dir", "", "Store directory")
2✔
371
        if err := fs.Parse(args); err != nil {
2✔
NEW
372
                return 1
×
NEW
373
        }
×
374

375
        storeDir, ok := c.requireDir(*dir)
2✔
376
        if !ok {
2✔
NEW
377
                return 1
×
NEW
378
        }
×
379

380
        opts := tinykvs.DefaultOptions(storeDir)
2✔
381
        store, err := tinykvs.Open(storeDir, opts)
2✔
382
        if err != nil {
2✔
NEW
383
                fmt.Fprintf(c.Stderr, msgErrOpenStore, err)
×
NEW
384
                return 1
×
NEW
385
        }
×
386
        defer store.Close()
2✔
387

2✔
388
        stats := store.Stats()
2✔
389

2✔
390
        fmt.Fprintf(c.Stdout, "Store: %s\n\n", storeDir)
2✔
391
        fmt.Fprintf(c.Stdout, "Memtable:\n")
2✔
392
        fmt.Fprintf(c.Stdout, "  Size:  %s\n", formatBytes(stats.MemtableSize))
2✔
393
        fmt.Fprintf(c.Stdout, "  Keys:  %d\n", stats.MemtableCount)
2✔
394

2✔
395
        fmt.Fprintf(c.Stdout, "\nIndex Memory: %s\n", formatBytes(stats.IndexMemory))
2✔
396

2✔
397
        fmt.Fprintf(c.Stdout, "\nCache:\n")
2✔
398
        fmt.Fprintf(c.Stdout, "  Size:    %s\n", formatBytes(stats.CacheStats.Size))
2✔
399
        fmt.Fprintf(c.Stdout, "  Entries: %d\n", stats.CacheStats.Entries)
2✔
400
        fmt.Fprintf(c.Stdout, "  Hits:    %d\n", stats.CacheStats.Hits)
2✔
401
        fmt.Fprintf(c.Stdout, "  Misses:  %d\n", stats.CacheStats.Misses)
2✔
402
        if stats.CacheStats.Hits+stats.CacheStats.Misses > 0 {
2✔
NEW
403
                hitRate := float64(stats.CacheStats.Hits) / float64(stats.CacheStats.Hits+stats.CacheStats.Misses) * 100
×
NEW
404
                fmt.Fprintf(c.Stdout, "  Hit Rate: %.1f%%\n", hitRate)
×
NEW
405
        }
×
406

407
        fmt.Fprintf(c.Stdout, "\nLevels:\n")
2✔
408
        var totalSize int64
2✔
409
        var totalKeys uint64
2✔
410
        var totalTables int
2✔
411
        for _, level := range stats.Levels {
16✔
412
                if level.NumTables > 0 {
16✔
413
                        fmt.Fprintf(c.Stdout, "  L%d: %3d tables, %10s, %12d keys\n",
2✔
414
                                level.Level, level.NumTables, formatBytes(level.Size), level.NumKeys)
2✔
415
                        totalSize += level.Size
2✔
416
                        totalKeys += level.NumKeys
2✔
417
                        totalTables += level.NumTables
2✔
418
                }
2✔
419
        }
420
        fmt.Fprintf(c.Stdout, "\nTotal: %d tables, %s, %d keys\n", totalTables, formatBytes(totalSize), totalKeys)
2✔
421
        return 0
2✔
422
}
423

424
func (c *CLI) cmdCompact(args []string) int {
2✔
425
        fs := flag.NewFlagSet("compact", flag.ContinueOnError)
2✔
426
        fs.SetOutput(c.Stderr)
2✔
427
        dir := fs.String("dir", "", "Store directory")
2✔
428
        if err := fs.Parse(args); err != nil {
2✔
NEW
429
                return 1
×
NEW
430
        }
×
431

432
        storeDir, ok := c.requireDir(*dir)
2✔
433
        if !ok {
2✔
NEW
434
                return 1
×
NEW
435
        }
×
436

437
        opts := tinykvs.DefaultOptions(storeDir)
2✔
438
        store, err := tinykvs.Open(storeDir, opts)
2✔
439
        if err != nil {
3✔
440
                fmt.Fprintf(c.Stderr, msgErrOpenStore, err)
1✔
441
                return 1
1✔
442
        }
1✔
443
        defer store.Close()
1✔
444

1✔
445
        // Show before stats
1✔
446
        statsBefore := store.Stats()
1✔
447
        var l0Before, l1Before int
1✔
448
        for _, level := range statsBefore.Levels {
8✔
449
                if level.Level == 0 {
8✔
450
                        l0Before = level.NumTables
1✔
451
                } else if level.Level == 1 {
8✔
452
                        l1Before = level.NumTables
1✔
453
                }
1✔
454
        }
455

456
        fmt.Fprintf(c.Stdout, "Before: L0=%d tables, L1=%d tables\n", l0Before, l1Before)
1✔
457
        fmt.Fprintln(c.Stdout, "Compacting...")
1✔
458

1✔
459
        if err := store.Compact(); err != nil {
1✔
NEW
460
                fmt.Fprintf(c.Stderr, "Error compacting: %v\n", err)
×
NEW
461
                return 1
×
NEW
462
        }
×
463

464
        // Show after stats
465
        statsAfter := store.Stats()
1✔
466
        var l0After, l1After int
1✔
467
        for _, level := range statsAfter.Levels {
8✔
468
                if level.Level == 0 {
8✔
469
                        l0After = level.NumTables
1✔
470
                } else if level.Level == 1 {
8✔
471
                        l1After = level.NumTables
1✔
472
                }
1✔
473
        }
474

475
        fmt.Fprintf(c.Stdout, "After:  L0=%d tables, L1=%d tables\n", l0After, l1After)
1✔
476
        fmt.Fprintln(c.Stdout, "Done")
1✔
477
        return 0
1✔
478
}
479

480
func (c *CLI) cmdRepair(args []string) int {
3✔
481
        fs := flag.NewFlagSet("repair", flag.ContinueOnError)
3✔
482
        fs.SetOutput(c.Stderr)
3✔
483
        dir := fs.String("dir", "", "Store directory")
3✔
484
        dryRun := fs.Bool("dry-run", false, "Show what would be deleted without deleting")
3✔
485
        if err := fs.Parse(args); err != nil {
3✔
NEW
486
                return 1
×
NEW
487
        }
×
488

489
        storeDir, ok := c.requireDir(*dir)
3✔
490
        if !ok {
3✔
NEW
491
                return 1
×
NEW
492
        }
×
493

494
        validIDs, code := c.loadValidTableIDs(storeDir)
3✔
495
        if code != 0 {
3✔
NEW
496
                return code
×
NEW
497
        }
×
498

499
        orphans, orphanSize, code := c.findOrphanFiles(storeDir, validIDs)
3✔
500
        if code != 0 {
3✔
NEW
501
                return code
×
NEW
502
        }
×
503

504
        if len(orphans) == 0 {
4✔
505
                fmt.Fprintln(c.Stdout, "No orphan files found")
1✔
506
                return 0
1✔
507
        }
1✔
508

509
        fmt.Fprintf(c.Stdout, "Found %d orphan files (%.2f GB)\n", len(orphans), float64(orphanSize)/1e9)
2✔
510

2✔
511
        if *dryRun {
3✔
512
                c.printDryRunOrphans(orphans)
1✔
513
                return 0
1✔
514
        }
1✔
515

516
        c.deleteOrphanFiles(storeDir, orphans)
1✔
517
        return 0
1✔
518
}
519

520
// loadValidTableIDs reads the manifest and returns a set of valid table IDs.
521
func (c *CLI) loadValidTableIDs(storeDir string) (map[uint32]bool, int) {
3✔
522
        manifestPath := filepath.Join(storeDir, "MANIFEST")
3✔
523
        manifest, err := tinykvs.OpenManifest(manifestPath)
3✔
524
        if err != nil {
3✔
NEW
525
                fmt.Fprintf(c.Stderr, "Error opening manifest: %v\n", err)
×
NEW
526
                return nil, 1
×
NEW
527
        }
×
528
        tables := manifest.Tables()
3✔
529
        manifest.Close()
3✔
530

3✔
531
        validIDs := make(map[uint32]bool)
3✔
532
        for _, meta := range tables {
6✔
533
                validIDs[meta.ID] = true
3✔
534
        }
3✔
535
        return validIDs, 0
3✔
536
}
537

538
// findOrphanFiles returns SST files not in the valid ID set.
539
func (c *CLI) findOrphanFiles(storeDir string, validIDs map[uint32]bool) ([]string, int64, int) {
3✔
540
        entries, err := os.ReadDir(storeDir)
3✔
541
        if err != nil {
3✔
NEW
542
                fmt.Fprintf(c.Stderr, "Error reading directory: %v\n", err)
×
NEW
543
                return nil, 0, 1
×
NEW
544
        }
×
545

546
        var orphans []string
3✔
547
        var orphanSize int64
3✔
548
        for _, e := range entries {
18✔
549
                if name, size, isOrphan := checkOrphanFile(e, validIDs); isOrphan {
18✔
550
                        orphans = append(orphans, name)
3✔
551
                        orphanSize += size
3✔
552
                }
3✔
553
        }
554
        return orphans, orphanSize, 0
3✔
555
}
556

557
// printDryRunOrphans prints orphan files without deleting.
558
func (c *CLI) printDryRunOrphans(orphans []string) {
1✔
559
        fmt.Fprintln(c.Stdout, "\nOrphan files (dry run - not deleting):")
1✔
560
        for _, name := range orphans {
2✔
561
                fmt.Fprintf(c.Stdout, "  %s\n", name)
1✔
562
        }
1✔
563
}
564

565
// deleteOrphanFiles deletes the given orphan files.
566
func (c *CLI) deleteOrphanFiles(storeDir string, orphans []string) {
1✔
567
        var deleted int
1✔
568
        var deletedSize int64
1✔
569
        for _, name := range orphans {
3✔
570
                path := filepath.Join(storeDir, name)
2✔
571
                info, _ := os.Stat(path)
2✔
572
                if err := os.Remove(path); err != nil {
2✔
NEW
573
                        fmt.Fprintf(c.Stderr, "Failed to delete %s: %v\n", name, err)
×
NEW
574
                        continue
×
575
                }
576
                deleted++
2✔
577
                deletedSize += info.Size()
2✔
578
                fmt.Fprintf(c.Stdout, "Deleted: %s\n", name)
2✔
579
        }
580
        fmt.Fprintf(c.Stdout, "\nDeleted %d files, recovered %.2f GB\n", deleted, float64(deletedSize)/1e9)
1✔
581
}
582

583
func (c *CLI) cmdInfo(args []string) int {
5✔
584
        fs := flag.NewFlagSet("info", flag.ContinueOnError)
5✔
585
        fs.SetOutput(c.Stderr)
5✔
586
        dir := fs.String("dir", "", "Store directory")
5✔
587
        key := fs.String("key", "", "Key to look up (string)")
5✔
588
        keyHex := fs.String(flagKeyHex, "", "Key to look up (hex encoded)")
5✔
589
        if err := fs.Parse(args); err != nil {
5✔
NEW
590
                return 1
×
NEW
591
        }
×
592

593
        storeDir, ok := c.requireDir(*dir)
5✔
594
        if !ok {
5✔
NEW
595
                return 1
×
NEW
596
        }
×
597

598
        var keyBytes []byte
5✔
599
        if *keyHex != "" {
7✔
600
                var err error
2✔
601
                keyBytes, err = hex.DecodeString(*keyHex)
2✔
602
                if err != nil {
3✔
603
                        fmt.Fprintf(c.Stderr, msgErrDecodeHexKey, err)
1✔
604
                        return 1
1✔
605
                }
1✔
606
        } else if *key != "" {
5✔
607
                keyBytes = []byte(*key)
2✔
608
        } else {
3✔
609
                fmt.Fprintln(c.Stderr, msgErrKeyRequired)
1✔
610
                fs.Usage()
1✔
611
                return 1
1✔
612
        }
1✔
613

614
        opts := tinykvs.DefaultOptions(storeDir)
3✔
615
        store, err := tinykvs.Open(storeDir, opts)
3✔
616
        if err != nil {
3✔
NEW
617
                fmt.Fprintf(c.Stderr, msgErrOpenStore, err)
×
NEW
618
                return 1
×
NEW
619
        }
×
620
        defer store.Close()
3✔
621

3✔
622
        // Check memtable first
3✔
623
        val, err := store.Get(keyBytes)
3✔
624
        if err == tinykvs.ErrKeyNotFound {
4✔
625
                fmt.Fprintln(c.Stdout, "Key not found")
1✔
626
                return 0
1✔
627
        }
1✔
628
        if err != nil {
2✔
NEW
629
                fmt.Fprintf(c.Stderr, msgErr, err)
×
NEW
630
                return 1
×
NEW
631
        }
×
632

633
        fmt.Fprintf(c.Stdout, "Key: %s\n", formatKey(keyBytes))
2✔
634
        fmt.Fprintf(c.Stdout, "Value type: %s\n", valueTypeName(val.Type))
2✔
635

2✔
636
        // Find which SSTable contains it
2✔
637
        location := store.FindKey(keyBytes)
2✔
638
        if location != nil {
4✔
639
                fmt.Fprintf(c.Stdout, "Location: L%d SSTable %06d\n", location.Level, location.TableID)
2✔
640
        } else {
2✔
NEW
641
                fmt.Fprintln(c.Stdout, "Location: memtable")
×
NEW
642
        }
×
643
        return 0
2✔
644
}
645

646
func (c *CLI) cmdExport(args []string) int {
5✔
647
        fs := flag.NewFlagSet("export", flag.ContinueOnError)
5✔
648
        fs.SetOutput(c.Stderr)
5✔
649
        dir := fs.String("dir", "", "Store directory")
5✔
650
        output := fs.String("output", "", "Output file path (required)")
5✔
651
        prefix := fs.String("prefix", "", "Only export keys with this prefix")
5✔
652
        prefixHex := fs.String("prefix-hex", "", "Only export keys with this prefix (hex)")
5✔
653
        if err := fs.Parse(args); err != nil {
5✔
NEW
654
                return 1
×
NEW
655
        }
×
656

657
        storeDir, ok := c.requireDir(*dir)
5✔
658
        if !ok {
5✔
NEW
659
                return 1
×
NEW
660
        }
×
661

662
        if *output == "" {
6✔
663
                fmt.Fprintln(c.Stderr, "Error: -output is required")
1✔
664
                fs.Usage()
1✔
665
                return 1
1✔
666
        }
1✔
667

668
        var prefixBytes []byte
4✔
669
        if *prefixHex != "" {
6✔
670
                var err error
2✔
671
                prefixBytes, err = hex.DecodeString(*prefixHex)
2✔
672
                if err != nil {
3✔
673
                        fmt.Fprintf(c.Stderr, "Error decoding hex prefix: %v\n", err)
1✔
674
                        return 1
1✔
675
                }
1✔
676
        } else if *prefix != "" {
2✔
NEW
677
                prefixBytes = []byte(*prefix)
×
NEW
678
        }
×
679

680
        opts := tinykvs.DefaultOptions(storeDir)
3✔
681
        store, err := tinykvs.Open(storeDir, opts)
3✔
682
        if err != nil {
3✔
NEW
683
                fmt.Fprintf(c.Stderr, msgErrOpenStore, err)
×
NEW
684
                return 1
×
NEW
685
        }
×
686
        defer store.Close()
3✔
687

3✔
688
        file, err := os.Create(*output)
3✔
689
        if err != nil {
3✔
NEW
690
                fmt.Fprintf(c.Stderr, "Error creating output file: %v\n", err)
×
NEW
691
                return 1
×
NEW
692
        }
×
693
        defer file.Close()
3✔
694

3✔
695
        writer := csv.NewWriter(file)
3✔
696
        defer writer.Flush()
3✔
697

3✔
698
        // Write header
3✔
699
        writer.Write([]string{"key", "type", "value"})
3✔
700

3✔
701
        var count int64
3✔
702
        err = store.ScanPrefix(prefixBytes, func(key []byte, val tinykvs.Value) bool {
9✔
703
                var valueStr string
6✔
704
                switch val.Type {
6✔
705
                case tinykvs.ValueTypeInt64:
2✔
706
                        valueStr = strconv.FormatInt(val.Int64, 10)
2✔
NEW
707
                case tinykvs.ValueTypeFloat64:
×
NEW
708
                        valueStr = strconv.FormatFloat(val.Float64, 'g', -1, 64)
×
NEW
709
                case tinykvs.ValueTypeBool:
×
NEW
710
                        valueStr = strconv.FormatBool(val.Bool)
×
711
                case tinykvs.ValueTypeString, tinykvs.ValueTypeBytes:
4✔
712
                        valueStr = hex.EncodeToString(val.Bytes)
4✔
713
                }
714

715
                writer.Write([]string{
6✔
716
                        hex.EncodeToString(key),
6✔
717
                        valueTypeName(val.Type),
6✔
718
                        valueStr,
6✔
719
                })
6✔
720

6✔
721
                count++
6✔
722
                if count%100000 == 0 {
6✔
NEW
723
                        fmt.Fprintf(c.Stderr, "\rExported %d keys...", count)
×
NEW
724
                }
×
725
                return true
6✔
726
        })
727

728
        if err != nil {
3✔
NEW
729
                fmt.Fprintf(c.Stderr, "\nError scanning: %v\n", err)
×
NEW
730
                return 1
×
NEW
731
        }
×
732

733
        fmt.Fprintf(c.Stderr, "\rExported %d keys to %s\n", count, *output)
3✔
734
        return 0
3✔
735
}
736

737
func (c *CLI) cmdImport(args []string) int {
3✔
738
        fs := flag.NewFlagSet("import", flag.ContinueOnError)
3✔
739
        fs.SetOutput(c.Stderr)
3✔
740
        dir := fs.String("dir", "", "Store directory")
3✔
741
        input := fs.String("input", "", "Input file path (required)")
3✔
742
        if err := fs.Parse(args); err != nil {
3✔
NEW
743
                return 1
×
NEW
744
        }
×
745

746
        storeDir, ok := c.requireDir(*dir)
3✔
747
        if !ok {
3✔
NEW
748
                return 1
×
NEW
749
        }
×
750

751
        if *input == "" {
4✔
752
                fmt.Fprintln(c.Stderr, "Error: -input is required")
1✔
753
                fs.Usage()
1✔
754
                return 1
1✔
755
        }
1✔
756

757
        file, err := os.Open(*input)
2✔
758
        if err != nil {
3✔
759
                fmt.Fprintf(c.Stderr, "Error opening input file: %v\n", err)
1✔
760
                return 1
1✔
761
        }
1✔
762
        defer file.Close()
1✔
763

1✔
764
        opts := tinykvs.DefaultOptions(storeDir)
1✔
765
        store, err := tinykvs.Open(storeDir, opts)
1✔
766
        if err != nil {
1✔
NEW
767
                fmt.Fprintf(c.Stderr, msgErrOpenStore, err)
×
NEW
768
                return 1
×
NEW
769
        }
×
770
        defer store.Close()
1✔
771

1✔
772
        count, errors := c.importCSVRecordsToStore(file, store)
1✔
773

1✔
774
        if err := store.Flush(); err != nil {
1✔
NEW
775
                fmt.Fprintf(c.Stderr, "\nError flushing: %v\n", err)
×
NEW
776
                return 1
×
NEW
777
        }
×
778

779
        fmt.Fprintf(c.Stderr, "\rImported %d keys (%d errors)\n", count, errors)
1✔
780
        return 0
1✔
781
}
782

783
// importCSVRecordsToStore reads CSV records and imports them to the store.
784
func (c *CLI) importCSVRecordsToStore(file *os.File, store *tinykvs.Store) (count, errors int64) {
1✔
785
        reader := csv.NewReader(bufio.NewReader(file))
1✔
786

1✔
787
        if _, err := reader.Read(); err != nil {
1✔
NEW
788
                fmt.Fprintf(c.Stderr, "Error reading header: %v\n", err)
×
NEW
789
                return 0, 1
×
NEW
790
        }
×
791

792
        for {
4✔
793
                record, err := reader.Read()
3✔
794
                if err == io.EOF {
4✔
795
                        break
1✔
796
                }
797
                if err != nil {
2✔
NEW
798
                        errors++
×
NEW
799
                        continue
×
800
                }
801

802
                if importSingleRecord(store, record) {
4✔
803
                        count++
2✔
804
                        if count%100000 == 0 {
2✔
NEW
805
                                fmt.Fprintf(c.Stderr, "\rImported %d keys...", count)
×
NEW
806
                        }
×
NEW
807
                } else {
×
NEW
808
                        errors++
×
NEW
809
                }
×
810
        }
811
        return count, errors
1✔
812
}
813

NEW
814
func (c *CLI) cmdShell(args []string) int {
×
NEW
815
        fs := flag.NewFlagSet("shell", flag.ContinueOnError)
×
NEW
816
        fs.SetOutput(c.Stderr)
×
NEW
817
        dir := fs.String("dir", "", "Store directory")
×
NEW
818
        if err := fs.Parse(args); err != nil {
×
NEW
819
                return 1
×
NEW
820
        }
×
821

NEW
822
        storeDir, ok := c.requireDir(*dir)
×
NEW
823
        if !ok {
×
NEW
824
                return 1
×
NEW
825
        }
×
826

NEW
827
        opts := tinykvs.DefaultOptions(storeDir)
×
NEW
828
        store, err := tinykvs.Open(storeDir, opts)
×
NEW
829
        if err != nil {
×
NEW
830
                fmt.Fprintf(c.Stderr, msgErrOpenStore, err)
×
NEW
831
                return 1
×
NEW
832
        }
×
NEW
833
        defer store.Close()
×
NEW
834

×
NEW
835
        shell := NewShell(store)
×
NEW
836
        shell.Run()
×
NEW
837
        return 0
×
838
}
839

840
func (c *CLI) printValue(key []byte, val tinykvs.Value) {
22✔
841
        fmt.Fprintf(c.Stdout, "%s = ", formatKey(key))
22✔
842
        switch val.Type {
22✔
843
        case tinykvs.ValueTypeInt64:
2✔
844
                fmt.Fprintf(c.Stdout, "(int64) %d\n", val.Int64)
2✔
845
        case tinykvs.ValueTypeFloat64:
2✔
846
                fmt.Fprintf(c.Stdout, "(float64) %f\n", val.Float64)
2✔
847
        case tinykvs.ValueTypeBool:
3✔
848
                fmt.Fprintf(c.Stdout, "(bool) %t\n", val.Bool)
3✔
849
        case tinykvs.ValueTypeString:
12✔
850
                fmt.Fprintf(c.Stdout, "(string) %q\n", string(val.Bytes))
12✔
851
        case tinykvs.ValueTypeBytes:
3✔
852
                if len(val.Bytes) <= 64 {
5✔
853
                        fmt.Fprintf(c.Stdout, "(bytes) %s\n", hex.EncodeToString(val.Bytes))
2✔
854
                } else {
3✔
855
                        fmt.Fprintf(c.Stdout, "(bytes) %s... (%d bytes)\n", hex.EncodeToString(val.Bytes[:64]), len(val.Bytes))
1✔
856
                }
1✔
NEW
857
        case tinykvs.ValueTypeTombstone:
×
NEW
858
                fmt.Fprintln(c.Stdout, "(deleted)")
×
NEW
859
        default:
×
NEW
860
                fmt.Fprintf(c.Stdout, "(unknown type %d)\n", val.Type)
×
861
        }
862
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc