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

freeeve / tinykvs / 21187550776

20 Jan 2026 09:16PM UTC coverage: 70.979% (-0.4%) from 71.335%
21187550776

push

github

freeeve
refactor: extract helpers to reduce cognitive complexity

- shell.go: Extract loadHistory, saveHistory, runLoop, handlePromptError
- wal.go: Extract ensureBlockSpace, prepareFragment, writeRecord, type helpers
- main.go: Extract loadValidTableIDs, findOrphanFiles, deleteOrphanFiles

34 of 109 new or added lines in 3 files covered. (31.19%)

11 existing lines in 4 files now uncovered.

5755 of 8108 relevant lines covered (70.98%)

403005.06 hits per line

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

54.6
/cmd/tinykvs/shell.go
1
package main
2

3
import (
4
        "fmt"
5
        "os"
6
        "path/filepath"
7
        "strconv"
8
        "strings"
9
        "time"
10

11
        "github.com/blastrain/vitess-sqlparser/sqlparser"
12
        "github.com/freeeve/tinykvs"
13
        "github.com/peterh/liner"
14
)
15

16
// Shell provides an interactive SQL-like query interface.
17
type Shell struct {
18
        store       *tinykvs.Store
19
        prompt      string
20
        historyFile string
21
        line        *liner.State
22
}
23

24
// NewShell creates a new shell instance.
25
func NewShell(store *tinykvs.Store) *Shell {
205✔
26
        // History file in user's home directory
205✔
27
        historyFile := ""
205✔
28
        if home, err := os.UserHomeDir(); err == nil {
410✔
29
                historyFile = filepath.Join(home, ".tinykvs_history")
205✔
30
        }
205✔
31

32
        return &Shell{
205✔
33
                store:       store,
205✔
34
                prompt:      "tinykvs> ",
205✔
35
                historyFile: historyFile,
205✔
36
        }
205✔
37
}
38

39
// Run starts the interactive shell.
40
func (s *Shell) Run() {
×
41
        s.line = liner.NewLiner()
×
42
        defer s.line.Close()
×
43

×
44
        s.line.SetCtrlCAborts(true)
×
NEW
45
        s.loadHistory()
×
46

×
47
        fmt.Println("TinyKVS Shell " + versionString())
×
48
        fmt.Println("Type \\help for help, \\q to quit")
×
49
        fmt.Println()
×
50

×
NEW
51
        s.runLoop()
×
NEW
52
        s.saveHistory()
×
NEW
53
}
×
54

55
// loadHistory loads shell history from file.
NEW
56
func (s *Shell) loadHistory() {
×
NEW
57
        if s.historyFile == "" {
×
NEW
58
                return
×
NEW
59
        }
×
NEW
60
        f, err := os.Open(s.historyFile)
×
NEW
61
        if err != nil {
×
NEW
62
                return
×
NEW
63
        }
×
NEW
64
        s.line.ReadHistory(f)
×
NEW
65
        f.Close()
×
66
}
67

68
// saveHistory saves shell history to file.
NEW
69
func (s *Shell) saveHistory() {
×
NEW
70
        if s.historyFile == "" {
×
NEW
71
                return
×
NEW
72
        }
×
NEW
73
        f, err := os.Create(s.historyFile)
×
NEW
74
        if err != nil {
×
NEW
75
                return
×
NEW
76
        }
×
NEW
77
        s.line.WriteHistory(f)
×
NEW
78
        f.Close()
×
79
}
80

81
// runLoop processes user input until exit.
NEW
82
func (s *Shell) runLoop() {
×
83
        for {
×
84
                input, err := s.line.Prompt(s.prompt)
×
85
                if err != nil {
×
NEW
86
                        if !s.handlePromptError(err) {
×
NEW
87
                                break
×
88
                        }
NEW
89
                        continue
×
90
                }
UNCOV
91
                input = strings.TrimSpace(input)
×
92
                if input == "" {
×
93
                        continue
×
94
                }
UNCOV
95
                s.line.AppendHistory(input)
×
UNCOV
96
                if !s.execute(input) {
×
97
                        break
×
98
                }
99
        }
100
}
101

102
// handlePromptError handles liner prompt errors. Returns true to continue.
NEW
103
func (s *Shell) handlePromptError(err error) bool {
×
NEW
104
        if err == liner.ErrPromptAborted {
×
NEW
105
                fmt.Println("^C")
×
NEW
106
                return true
×
UNCOV
107
        }
×
NEW
108
        fmt.Println()
×
NEW
109
        return false
×
110
}
111

112
// execute runs a command. Returns false to exit.
113
func (s *Shell) execute(line string) bool {
479✔
114
        // Handle shell commands
479✔
115
        if strings.HasPrefix(line, "\\") {
535✔
116
                return s.handleCommand(line)
56✔
117
        }
56✔
118

119
        // Remove trailing semicolon if present
120
        line = strings.TrimSuffix(line, ";")
423✔
121

423✔
122
        // Pre-process SQL functions (uint64_be, byte, fnv64, etc.) and concatenation
423✔
123
        line = preprocessFunctions(line)
423✔
124

423✔
125
        // Pre-process: convert "STARTS WITH x'...'" to "LIKE '$$HEX$$...%'"
423✔
126
        // and "STARTS WITH '...'" to "LIKE '...%'"
423✔
127
        line = preprocessStartsWith(line)
423✔
128

423✔
129
        // Extract ORDER BY before SQL parsing (parser doesn't support ORDER BY for kv)
423✔
130
        line, orderBy := ParseOrderBy(line)
423✔
131

423✔
132
        // Parse SQL
423✔
133
        stmt, err := sqlparser.Parse(line)
423✔
134
        if err != nil {
427✔
135
                fmt.Printf("Parse error: %v\n", err)
4✔
136
                return true
4✔
137
        }
4✔
138

139
        switch st := stmt.(type) {
419✔
140
        case *sqlparser.Select:
132✔
141
                s.handleSelect(st, orderBy)
132✔
142
        case *sqlparser.Insert:
268✔
143
                s.handleInsert(st)
268✔
144
        case *sqlparser.Update:
6✔
145
                s.handleUpdate(st)
6✔
146
        case *sqlparser.Delete:
13✔
147
                s.handleDelete(st)
13✔
148
        default:
×
149
                fmt.Printf("Unsupported statement type: %T\n", stmt)
×
150
        }
151

152
        return true
419✔
153
}
154

155
func (s *Shell) handleCommand(cmd string) bool {
56✔
156
        parts := strings.Fields(cmd)
56✔
157
        if len(parts) == 0 {
56✔
158
                return true
×
159
        }
×
160

161
        switch parts[0] {
56✔
162
        case "\\q", "\\quit", "\\exit":
6✔
163
                fmt.Println("Bye")
6✔
164
                return false
6✔
165
        case "\\help", "\\h", "\\?":
3✔
166
                s.printHelp()
3✔
167
        case "\\stats":
3✔
168
                s.printStats()
3✔
169
        case "\\compact":
3✔
170
                fmt.Println("Compacting...")
3✔
171
                if err := s.store.Compact(); err != nil {
4✔
172
                        fmt.Printf("Error: %v\n", err)
1✔
173
                } else {
3✔
174
                        fmt.Println("Done")
2✔
175
                }
2✔
176
        case "\\flush":
8✔
177
                if err := s.store.Flush(); err != nil {
9✔
178
                        fmt.Printf("Error: %v\n", err)
1✔
179
                } else {
8✔
180
                        fmt.Println("Flushed")
7✔
181
                }
7✔
182
        case "\\tables":
2✔
183
                fmt.Println("Table: kv (k TEXT, v TEXT)")
2✔
184
                fmt.Println("  - k: the key (string or hex with x'...')")
2✔
185
                fmt.Println("  - v: the value")
2✔
186
        case "\\explain":
×
187
                if len(parts) < 2 {
×
188
                        fmt.Println("Usage: \\explain <prefix>")
×
189
                        fmt.Println("  Shows which SSTables contain keys with the given prefix")
×
190
                        return true
×
191
                }
×
192
                s.explainPrefix(parts[1])
×
193
        case "\\export":
11✔
194
                if len(parts) < 2 {
12✔
195
                        fmt.Println("Usage: \\export <filename.csv>")
1✔
196
                        return true
1✔
197
                }
1✔
198
                s.exportCSV(parts[1])
10✔
199
        case "\\import":
18✔
200
                if len(parts) < 2 {
19✔
201
                        fmt.Println("Usage: \\import <filename.csv>")
1✔
202
                        return true
1✔
203
                }
1✔
204
                s.importCSV(parts[1])
17✔
205
        default:
2✔
206
                fmt.Printf("Unknown command: %s\n", parts[0])
2✔
207
                fmt.Println("Type \\help for help")
2✔
208
        }
209
        return true
48✔
210
}
211

212
func (s *Shell) printHelp() {
3✔
213
        fmt.Println(`SQL Commands:
3✔
214
  SELECT * FROM kv WHERE k = 'mykey'
3✔
215
  SELECT * FROM kv WHERE k LIKE 'prefix%'
3✔
216
  SELECT * FROM kv WHERE k BETWEEN 'a' AND 'z' LIMIT 10
3✔
217
  SELECT * FROM kv LIMIT 100
3✔
218
  SELECT v.name, v.age FROM kv WHERE k = 'user:1'    -- record fields
3✔
219
  SELECT v.address.city FROM kv WHERE k = 'user:1'   -- nested fields
3✔
220

3✔
221
  ORDER BY (buffers results for sorting):
3✔
222
  SELECT * FROM kv ORDER BY k DESC LIMIT 10
3✔
223
  SELECT v.name, v.age FROM kv ORDER BY v.age DESC, v.name
3✔
224
  SELECT * FROM kv WHERE k LIKE 'user:%' ORDER BY v.score LIMIT 100
3✔
225

3✔
226
  Aggregations (streaming):
3✔
227
  SELECT count() FROM kv
3✔
228
  SELECT count(), sum(v.age), avg(v.age) FROM kv
3✔
229
  SELECT min(v.score), max(v.score) FROM kv WHERE k LIKE 'user:%'
3✔
230

3✔
231
  INSERT INTO kv (k, v) VALUES ('mykey', 'myvalue')
3✔
232
  INSERT INTO kv VALUES ('mykey', 'myvalue')
3✔
233
  INSERT INTO kv VALUES ('user:1', '{"name":"Alice","age":30}')  -- JSON record
3✔
234
  INSERT INTO kv VALUES ('user:2', x'82a46e616d65a3426f62a361676514')  -- msgpack
3✔
235

3✔
236
  UPDATE kv SET v = 'newvalue' WHERE k = 'mykey'
3✔
237

3✔
238
  DELETE FROM kv WHERE k = 'mykey'
3✔
239
  DELETE FROM kv WHERE k LIKE 'prefix%'
3✔
240

3✔
241
Shell Commands:
3✔
242
  \help, \h, \?      Show this help
3✔
243
  \stats             Show store statistics
3✔
244
  \explain <prefix>  Show which SSTables contain a prefix
3✔
245
  \compact           Run compaction
3✔
246
  \flush             Flush memtable to disk
3✔
247
  \tables            Show table schema
3✔
248
  \export <file>     Export to CSV (key,value format)
3✔
249
  \import <file>     Import from CSV (auto-detects format)
3✔
250
  \q, \quit          Exit shell
3✔
251

3✔
252
CSV Import Formats:
3✔
253
  key,value              2 columns: key + value (auto-detects type)
3✔
254
  key,col1,col2,...      3+ columns: key + fields become a record
3✔
255
  key,name:string,age:int  Type hints: string, int, float, bool, json
3✔
256

3✔
257
Notes:
3✔
258
  - Use single quotes for strings: 'mykey'
3✔
259
  - Use x'...' for hex values: x'deadbeef'
3✔
260
  - JSON strings are auto-detected and stored as records
3✔
261
  - Hex values starting with msgpack map markers are stored as records
3✔
262
  - Access record fields with v.fieldname in SELECT
3✔
263
  - Nested fields: v.a.b (2 levels) or v.` + "`a.b.c`" + ` (deeper)
3✔
264
  - LIKE only supports prefix matching (trailing %)
3✔
265
  - All operations are on the virtual 'kv' table
3✔
266
  - Use up/down arrows to navigate command history`)
3✔
267
}
3✔
268

269
func (s *Shell) printStats() {
3✔
270
        stats := s.store.Stats()
3✔
271

3✔
272
        fmt.Printf("Memtable: %d keys, %s\n", stats.MemtableCount, formatBytes(stats.MemtableSize))
3✔
273
        fmt.Printf("Cache: %d entries, %s (%.1f%% hit rate)\n",
3✔
274
                stats.CacheStats.Entries, formatBytes(stats.CacheStats.Size),
3✔
275
                cacheHitRate(stats.CacheStats))
3✔
276

3✔
277
        var totalKeys uint64
3✔
278
        var totalSize int64
3✔
279
        for _, level := range stats.Levels {
24✔
280
                if level.NumTables > 0 {
22✔
281
                        fmt.Printf("L%d: %d tables, %d keys, %s\n",
1✔
282
                                level.Level, level.NumTables, level.NumKeys, formatBytes(level.Size))
1✔
283
                        totalKeys += level.NumKeys
1✔
284
                        totalSize += level.Size
1✔
285
                }
1✔
286
        }
287
        fmt.Printf("Total: %d keys, %s\n", totalKeys, formatBytes(totalSize))
3✔
288
}
289

290
func (s *Shell) explainPrefix(prefixStr string) {
×
291
        prefix := parseHexPrefix(prefixStr)
×
292

×
293
        tables := s.store.ExplainPrefix(prefix)
×
294

×
295
        if len(tables) == 0 {
×
296
                fmt.Printf("No tables have prefix %x in their key range\n", prefix)
×
297
                return
×
298
        }
×
299

300
        levelTables := groupTablesByLevel(tables)
×
301

×
302
        fmt.Printf("Tables with prefix %x in range:\n", prefix)
×
303
        fmt.Println()
×
304

×
305
        var totalInRange, totalWithMatch int
×
306
        for level := 0; level < 7; level++ {
×
307
                lt := levelTables[level]
×
308
                if len(lt) == 0 {
×
309
                        continue
×
310
                }
311

312
                inRange, withMatch := printLevelDetails(level, lt)
×
313
                totalInRange += inRange
×
314
                totalWithMatch += withMatch
×
315
        }
316

317
        fmt.Println()
×
318
        fmt.Printf("Summary: %d tables in range, %d with actual matches\n", totalInRange, totalWithMatch)
×
319
}
320

321
// parseHexPrefix parses a prefix string, handling hex formats (0x..., x'...').
322
func parseHexPrefix(prefixStr string) []byte {
×
323
        if strings.HasPrefix(prefixStr, "0x") || strings.HasPrefix(prefixStr, "0X") {
×
324
                return parseHexString(prefixStr[2:])
×
325
        }
×
326
        if strings.HasPrefix(prefixStr, "x'") && strings.HasSuffix(prefixStr, "'") {
×
327
                return parseHexString(prefixStr[2 : len(prefixStr)-1])
×
328
        }
×
329
        return []byte(prefixStr)
×
330
}
331

332
// parseHexString converts a hex string to bytes.
333
func parseHexString(hexStr string) []byte {
×
334
        prefix := make([]byte, len(hexStr)/2)
×
335
        for i := 0; i < len(prefix); i++ {
×
336
                fmt.Sscanf(hexStr[i*2:i*2+2], "%02x", &prefix[i])
×
337
        }
×
338
        return prefix
×
339
}
340

341
// groupTablesByLevel organizes tables into a map by their level.
342
func groupTablesByLevel(tables []tinykvs.PrefixTableInfo) map[int][]tinykvs.PrefixTableInfo {
×
343
        levelTables := make(map[int][]tinykvs.PrefixTableInfo)
×
344
        for _, t := range tables {
×
345
                levelTables[t.Level] = append(levelTables[t.Level], t)
×
346
        }
×
347
        return levelTables
×
348
}
349

350
// printLevelDetails prints information about tables at a given level and returns counts.
351
func printLevelDetails(level int, tables []tinykvs.PrefixTableInfo) (inRange, withMatch int) {
×
352
        matchCount := 0
×
353
        for _, t := range tables {
×
354
                if t.HasMatch {
×
355
                        matchCount++
×
356
                }
×
357
        }
358

359
        fmt.Printf("L%d: %d tables in range, %d with matching keys\n", level, len(tables), matchCount)
×
360

×
361
        shown := 0
×
362
        for _, t := range tables {
×
363
                if t.HasMatch && shown < 10 {
×
364
                        fmt.Printf("  [%d] minKey=%x maxKey=%x firstMatch=%x (%d keys)\n",
×
365
                                t.TableID, t.MinKey, t.MaxKey, t.FirstMatch, t.NumKeys)
×
366
                        shown++
×
367
                }
×
368
        }
369
        if matchCount > 10 {
×
370
                fmt.Printf("  ... and %d more tables with matches\n", matchCount-10)
×
371
        }
×
372

373
        return len(tables), matchCount
×
374
}
375

376
func cacheHitRate(cs tinykvs.CacheStats) float64 {
5✔
377
        total := cs.Hits + cs.Misses
5✔
378
        if total == 0 {
9✔
379
                return 0
4✔
380
        }
4✔
381
        return float64(cs.Hits) / float64(total) * 100
1✔
382
}
383

384
// formatIntCommas formats an integer with comma separators for readability.
385
func formatIntCommas(n int64) string {
106✔
386
        if n < 0 {
106✔
387
                return "-" + formatIntCommas(-n)
×
388
        }
×
389
        s := strconv.FormatInt(n, 10)
106✔
390
        if len(s) <= 3 {
212✔
391
                return s
106✔
392
        }
106✔
393
        // Insert commas from right to left
394
        var result strings.Builder
×
395
        for i, c := range s {
×
396
                if i > 0 && (len(s)-i)%3 == 0 {
×
397
                        result.WriteByte(',')
×
398
                }
×
399
                result.WriteRune(c)
×
400
        }
401
        return result.String()
×
402
}
403

404
// formatDuration formats a duration in human-readable form (e.g., "3m 7s", "2.3s").
405
func formatDuration(d time.Duration) string {
106✔
406
        if d < time.Second {
212✔
407
                return fmt.Sprintf("%dms", d.Milliseconds())
106✔
408
        }
106✔
409
        if d < time.Minute {
×
410
                return fmt.Sprintf("%.1fs", d.Seconds())
×
411
        }
×
412
        minutes := int(d.Minutes())
×
413
        seconds := int(d.Seconds()) % 60
×
414
        if minutes < 60 {
×
415
                return fmt.Sprintf("%dm %ds", minutes, seconds)
×
416
        }
×
417
        hours := minutes / 60
×
418
        minutes = minutes % 60
×
419
        return fmt.Sprintf("%dh %dm %ds", hours, minutes, seconds)
×
420
}
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