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

freeeve / tinykvs / 21083691545

16 Jan 2026 11:05PM UTC coverage: 62.888% (-20.1%) from 83.01%
21083691545

push

github

freeeve
feat: interactive shell, aggregations, and batch operations

Interactive Shell:
- SQL-like query language (SELECT, WHERE, ORDER BY, LIMIT)
- Aggregation functions: COUNT, SUM, AVG, MIN, MAX
- Support for nested field access (e.g., payment.amount)
- Tab completion and history

Batch Operations:
- WriteBatch for atomic multi-key writes
- PutIfNotExists (insert only if key doesnt exist)
- PutIfEquals (compare-and-swap)
- Increment (atomic counter updates)
- DeleteRange and DeletePrefix

Code Organization:
- Split store.go into batch.go, query.go, convenience.go
- Split store_test.go into focused test files
- Applied gofmt -s simplifications

API Additions:
- Store.Count, Sum, Avg, Min, Max, Aggregate methods
- Programmatic access to aggregation queries
- Batch.PutMap, PutStruct, PutJson helpers

Performance:
- Direct msgpack serialization for maps (2.5x faster PutMap)
- Reduced allocations in struct serialization

1203 of 2008 new or added lines in 9 files covered. (59.91%)

4294 of 6828 relevant lines covered (62.89%)

453042.62 hits per line

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

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

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

16
        "github.com/blastrain/vitess-sqlparser/sqlparser"
17
        "github.com/freeeve/tinykvs"
18
        "github.com/peterh/liner"
19
        "github.com/vmihailenco/msgpack/v5"
20
)
21

22
// Shell provides an interactive SQL-like query interface.
23
type Shell struct {
24
        store       *tinykvs.Store
25
        prompt      string
26
        historyFile string
27
        line        *liner.State
28
}
29

30
// aggType represents an aggregation function type.
31
type aggType int
32

33
const (
34
        aggCount aggType = iota
35
        aggSum
36
        aggAvg
37
        aggMin
38
        aggMax
39
)
40

41
// aggregator holds state for a streaming aggregation.
42
type aggregator struct {
43
        typ   aggType
44
        field string // field path for sum/avg/min/max (empty for count)
45
        alias string // display name
46

47
        // streaming state
48
        count    int64
49
        sum      float64
50
        min      float64
51
        max      float64
52
        hasValue bool
53
}
54

55
func (a *aggregator) update(val tinykvs.Value) {
44✔
56
        a.count++
44✔
57

44✔
58
        if a.field == "" {
57✔
59
                return // count() doesn't need field extraction
13✔
60
        }
13✔
61

62
        // Extract numeric value from field
63
        num, ok := a.extractNumeric(val)
31✔
64
        if !ok {
31✔
NEW
65
                return
×
NEW
66
        }
×
67

68
        a.sum += num
31✔
69
        if !a.hasValue {
40✔
70
                a.min = num
9✔
71
                a.max = num
9✔
72
                a.hasValue = true
9✔
73
        } else {
31✔
74
                if num < a.min {
24✔
75
                        a.min = num
2✔
76
                }
2✔
77
                if num > a.max {
40✔
78
                        a.max = num
18✔
79
                }
18✔
80
        }
81
}
82

83
func (a *aggregator) extractNumeric(val tinykvs.Value) (float64, bool) {
31✔
84
        // For non-record types, use the value directly
31✔
85
        if a.field == "" {
31✔
NEW
86
                switch val.Type {
×
NEW
87
                case tinykvs.ValueTypeInt64:
×
NEW
88
                        return float64(val.Int64), true
×
NEW
89
                case tinykvs.ValueTypeFloat64:
×
NEW
90
                        return val.Float64, true
×
NEW
91
                default:
×
NEW
92
                        return 0, false
×
93
                }
94
        }
95

96
        // For record/msgpack types, extract the field
97
        var record map[string]any
31✔
98
        switch val.Type {
31✔
NEW
99
        case tinykvs.ValueTypeRecord:
×
NEW
100
                record = val.Record
×
101
        case tinykvs.ValueTypeMsgpack:
31✔
102
                if err := msgpack.Unmarshal(val.Bytes, &record); err != nil {
31✔
NEW
103
                        return 0, false
×
NEW
104
                }
×
NEW
105
        default:
×
NEW
106
                return 0, false
×
107
        }
108
        if record == nil {
31✔
NEW
109
                return 0, false
×
NEW
110
        }
×
111

112
        fieldVal, ok := extractNestedField(record, a.field)
31✔
113
        if !ok {
31✔
NEW
114
                return 0, false
×
NEW
115
        }
×
116

117
        switch v := fieldVal.(type) {
31✔
NEW
118
        case int:
×
NEW
119
                return float64(v), true
×
120
        case int8:
11✔
121
                return float64(v), true
11✔
NEW
122
        case int16:
×
NEW
123
                return float64(v), true
×
NEW
124
        case int32:
×
NEW
125
                return float64(v), true
×
NEW
126
        case int64:
×
NEW
127
                return float64(v), true
×
NEW
128
        case uint:
×
NEW
129
                return float64(v), true
×
NEW
130
        case uint8:
×
NEW
131
                return float64(v), true
×
NEW
132
        case uint16:
×
NEW
133
                return float64(v), true
×
NEW
134
        case uint32:
×
NEW
135
                return float64(v), true
×
NEW
136
        case uint64:
×
NEW
137
                return float64(v), true
×
NEW
138
        case float32:
×
NEW
139
                return float64(v), true
×
140
        case float64:
20✔
141
                return v, true
20✔
NEW
142
        default:
×
NEW
143
                return 0, false
×
144
        }
145
}
146

147
func (a *aggregator) result() string {
15✔
148
        switch a.typ {
15✔
149
        case aggCount:
4✔
150
                return fmt.Sprintf("%d", a.count)
4✔
151
        case aggSum:
4✔
152
                if !a.hasValue {
5✔
153
                        return "NULL"
1✔
154
                }
1✔
155
                return formatNumber(a.sum)
3✔
156
        case aggAvg:
3✔
157
                if a.count == 0 || !a.hasValue {
4✔
158
                        return "NULL"
1✔
159
                }
1✔
160
                return formatNumber(a.sum / float64(a.count))
2✔
161
        case aggMin:
2✔
162
                if !a.hasValue {
2✔
NEW
163
                        return "NULL"
×
NEW
164
                }
×
165
                return formatNumber(a.min)
2✔
166
        case aggMax:
2✔
167
                if !a.hasValue {
2✔
NEW
168
                        return "NULL"
×
NEW
169
                }
×
170
                return formatNumber(a.max)
2✔
NEW
171
        default:
×
NEW
172
                return "NULL"
×
173
        }
174
}
175

176
func formatNumber(f float64) string {
9✔
177
        if f == math.Trunc(f) {
17✔
178
                return fmt.Sprintf("%.0f", f)
8✔
179
        }
8✔
180
        return fmt.Sprintf("%g", f)
1✔
181
}
182

183
// NewShell creates a new shell instance.
184
func NewShell(store *tinykvs.Store) *Shell {
157✔
185
        // History file in user's home directory
157✔
186
        historyFile := ""
157✔
187
        if home, err := os.UserHomeDir(); err == nil {
314✔
188
                historyFile = filepath.Join(home, ".tinykvs_history")
157✔
189
        }
157✔
190

191
        return &Shell{
157✔
192
                store:       store,
157✔
193
                prompt:      "tinykvs> ",
157✔
194
                historyFile: historyFile,
157✔
195
        }
157✔
196
}
197

198
// Run starts the interactive shell.
NEW
199
func (s *Shell) Run() {
×
NEW
200
        s.line = liner.NewLiner()
×
NEW
201
        defer s.line.Close()
×
NEW
202

×
NEW
203
        s.line.SetCtrlCAborts(true)
×
NEW
204

×
NEW
205
        // Load history
×
NEW
206
        if s.historyFile != "" {
×
NEW
207
                if f, err := os.Open(s.historyFile); err == nil {
×
NEW
208
                        s.line.ReadHistory(f)
×
NEW
209
                        f.Close()
×
NEW
210
                }
×
211
        }
212

NEW
213
        fmt.Println("TinyKVS Shell")
×
NEW
214
        fmt.Println("Type \\help for help, \\q to quit")
×
NEW
215
        fmt.Println()
×
NEW
216

×
NEW
217
        for {
×
NEW
218
                input, err := s.line.Prompt(s.prompt)
×
NEW
219
                if err != nil {
×
NEW
220
                        if err == liner.ErrPromptAborted {
×
NEW
221
                                fmt.Println("^C")
×
NEW
222
                                continue
×
223
                        }
NEW
224
                        break // EOF or other error
×
225
                }
226

NEW
227
                input = strings.TrimSpace(input)
×
NEW
228
                if input == "" {
×
NEW
229
                        continue
×
230
                }
231

232
                // Add to history
NEW
233
                s.line.AppendHistory(input)
×
NEW
234

×
NEW
235
                if !s.execute(input) {
×
NEW
236
                        break
×
237
                }
238
        }
239

240
        // Save history
NEW
241
        if s.historyFile != "" {
×
NEW
242
                if f, err := os.Create(s.historyFile); err == nil {
×
NEW
243
                        s.line.WriteHistory(f)
×
NEW
244
                        f.Close()
×
NEW
245
                }
×
246
        }
247
}
248

249
// execute runs a command. Returns false to exit.
250
func (s *Shell) execute(line string) bool {
350✔
251
        // Handle shell commands
350✔
252
        if strings.HasPrefix(line, "\\") {
408✔
253
                return s.handleCommand(line)
58✔
254
        }
58✔
255

256
        // Remove trailing semicolon if present
257
        line = strings.TrimSuffix(line, ";")
292✔
258

292✔
259
        // Parse SQL
292✔
260
        stmt, err := sqlparser.Parse(line)
292✔
261
        if err != nil {
293✔
262
                fmt.Printf("Parse error: %v\n", err)
1✔
263
                return true
1✔
264
        }
1✔
265

266
        switch st := stmt.(type) {
291✔
267
        case *sqlparser.Select:
94✔
268
                s.handleSelect(st)
94✔
269
        case *sqlparser.Insert:
178✔
270
                s.handleInsert(st)
178✔
271
        case *sqlparser.Update:
6✔
272
                s.handleUpdate(st)
6✔
273
        case *sqlparser.Delete:
13✔
274
                s.handleDelete(st)
13✔
NEW
275
        default:
×
NEW
276
                fmt.Printf("Unsupported statement type: %T\n", stmt)
×
277
        }
278

279
        return true
291✔
280
}
281

282
func (s *Shell) handleCommand(cmd string) bool {
58✔
283
        parts := strings.Fields(cmd)
58✔
284
        if len(parts) == 0 {
58✔
NEW
285
                return true
×
NEW
286
        }
×
287

288
        switch parts[0] {
58✔
289
        case "\\q", "\\quit", "\\exit":
6✔
290
                fmt.Println("Bye")
6✔
291
                return false
6✔
292
        case "\\help", "\\h", "\\?":
3✔
293
                s.printHelp()
3✔
294
        case "\\stats":
3✔
295
                s.printStats()
3✔
296
        case "\\compact":
3✔
297
                fmt.Println("Compacting...")
3✔
298
                if err := s.store.Compact(); err != nil {
4✔
299
                        fmt.Printf("Error: %v\n", err)
1✔
300
                } else {
3✔
301
                        fmt.Println("Done")
2✔
302
                }
2✔
303
        case "\\flush":
8✔
304
                if err := s.store.Flush(); err != nil {
9✔
305
                        fmt.Printf("Error: %v\n", err)
1✔
306
                } else {
8✔
307
                        fmt.Println("Flushed")
7✔
308
                }
7✔
309
        case "\\tables":
2✔
310
                fmt.Println("Table: kv (k TEXT, v TEXT)")
2✔
311
                fmt.Println("  - k: the key (string or hex with x'...')")
2✔
312
                fmt.Println("  - v: the value")
2✔
313
        case "\\export":
12✔
314
                if len(parts) < 2 {
13✔
315
                        fmt.Println("Usage: \\export <filename.csv>")
1✔
316
                        return true
1✔
317
                }
1✔
318
                s.exportCSV(parts[1])
11✔
319
        case "\\import":
19✔
320
                if len(parts) < 2 {
20✔
321
                        fmt.Println("Usage: \\import <filename.csv>")
1✔
322
                        return true
1✔
323
                }
1✔
324
                s.importCSV(parts[1])
18✔
325
        default:
2✔
326
                fmt.Printf("Unknown command: %s\n", parts[0])
2✔
327
                fmt.Println("Type \\help for help")
2✔
328
        }
329
        return true
50✔
330
}
331

332
func (s *Shell) printHelp() {
3✔
333
        fmt.Println(`SQL Commands:
3✔
334
  SELECT * FROM kv WHERE k = 'mykey'
3✔
335
  SELECT * FROM kv WHERE k LIKE 'prefix%'
3✔
336
  SELECT * FROM kv WHERE k BETWEEN 'a' AND 'z' LIMIT 10
3✔
337
  SELECT * FROM kv LIMIT 100
3✔
338
  SELECT v.name, v.age FROM kv WHERE k = 'user:1'    -- record fields
3✔
339
  SELECT v.address.city FROM kv WHERE k = 'user:1'   -- nested fields
3✔
340

3✔
341
  Aggregations (streaming):
3✔
342
  SELECT count() FROM kv
3✔
343
  SELECT count(), sum(v.age), avg(v.age) FROM kv
3✔
344
  SELECT min(v.score), max(v.score) FROM kv WHERE k LIKE 'user:%'
3✔
345

3✔
346
  INSERT INTO kv (k, v) VALUES ('mykey', 'myvalue')
3✔
347
  INSERT INTO kv VALUES ('mykey', 'myvalue')
3✔
348
  INSERT INTO kv VALUES ('user:1', '{"name":"Alice","age":30}')  -- JSON record
3✔
349
  INSERT INTO kv VALUES ('user:2', x'82a46e616d65a3426f62a361676514')  -- msgpack
3✔
350

3✔
351
  UPDATE kv SET v = 'newvalue' WHERE k = 'mykey'
3✔
352

3✔
353
  DELETE FROM kv WHERE k = 'mykey'
3✔
354
  DELETE FROM kv WHERE k LIKE 'prefix%'
3✔
355

3✔
356
Shell Commands:
3✔
357
  \help, \h, \?      Show this help
3✔
358
  \stats             Show store statistics
3✔
359
  \compact           Run compaction
3✔
360
  \flush             Flush memtable to disk
3✔
361
  \tables            Show table schema
3✔
362
  \export <file>     Export to CSV (key,value format)
3✔
363
  \import <file>     Import from CSV (auto-detects format)
3✔
364
  \q, \quit          Exit shell
3✔
365

3✔
366
CSV Import Formats:
3✔
367
  key,value              2 columns: key + value (auto-detects type)
3✔
368
  key,col1,col2,...      3+ columns: key + fields become a record
3✔
369
  key,name:string,age:int  Type hints: string, int, float, bool, json
3✔
370

3✔
371
Notes:
3✔
372
  - Use single quotes for strings: 'mykey'
3✔
373
  - Use x'...' for hex values: x'deadbeef'
3✔
374
  - JSON strings are auto-detected and stored as records
3✔
375
  - Hex values starting with msgpack map markers are stored as records
3✔
376
  - Access record fields with v.fieldname in SELECT
3✔
377
  - Nested fields: v.a.b (2 levels) or v.` + "`a.b.c`" + ` (deeper)
3✔
378
  - LIKE only supports prefix matching (trailing %)
3✔
379
  - All operations are on the virtual 'kv' table
3✔
380
  - Use up/down arrows to navigate command history`)
3✔
381
}
3✔
382

383
func (s *Shell) printStats() {
3✔
384
        stats := s.store.Stats()
3✔
385

3✔
386
        fmt.Printf("Memtable: %d keys, %s\n", stats.MemtableCount, formatBytes(stats.MemtableSize))
3✔
387
        fmt.Printf("Cache: %d entries, %s (%.1f%% hit rate)\n",
3✔
388
                stats.CacheStats.Entries, formatBytes(stats.CacheStats.Size),
3✔
389
                cacheHitRate(stats.CacheStats))
3✔
390

3✔
391
        var totalKeys uint64
3✔
392
        var totalSize int64
3✔
393
        for _, level := range stats.Levels {
24✔
394
                if level.NumTables > 0 {
22✔
395
                        fmt.Printf("L%d: %d tables, %d keys, %s\n",
1✔
396
                                level.Level, level.NumTables, level.NumKeys, formatBytes(level.Size))
1✔
397
                        totalKeys += level.NumKeys
1✔
398
                        totalSize += level.Size
1✔
399
                }
1✔
400
        }
401
        fmt.Printf("Total: %d keys, %s\n", totalKeys, formatBytes(totalSize))
3✔
402
}
403

404
func cacheHitRate(cs tinykvs.CacheStats) float64 {
5✔
405
        total := cs.Hits + cs.Misses
5✔
406
        if total == 0 {
9✔
407
                return 0
4✔
408
        }
4✔
409
        return float64(cs.Hits) / float64(total) * 100
1✔
410
}
411

412
func (s *Shell) handleSelect(stmt *sqlparser.Select) {
94✔
413
        // Parse WHERE clause
94✔
414
        var keyEquals string
94✔
415
        var keyPrefix string
94✔
416
        var keyStart, keyEnd string
94✔
417
        limit := 100 // default limit
94✔
418

94✔
419
        // Parse SELECT fields and aggregates
94✔
420
        var fields []string
94✔
421
        var aggs []*aggregator
94✔
422
        for _, expr := range stmt.SelectExprs {
199✔
423
                switch e := expr.(type) {
105✔
424
                case *sqlparser.AliasedExpr:
42✔
425
                        // Check for aggregate functions
42✔
426
                        if funcExpr, ok := e.Expr.(*sqlparser.FuncExpr); ok {
57✔
427
                                agg := parseAggregateFunc(funcExpr)
15✔
428
                                if agg != nil {
30✔
429
                                        aggs = append(aggs, agg)
15✔
430
                                        continue
15✔
431
                                }
432
                        }
433

434
                        // Check for field access (v.name, v.address.city)
435
                        if col, ok := e.Expr.(*sqlparser.ColName); ok {
54✔
436
                                qualifier := strings.ToLower(col.Qualifier.Name.String())
27✔
437
                                fieldName := col.Name.String()
27✔
438

27✔
439
                                if qualifier == "v" {
50✔
440
                                        // Direct v.field or v.`nested.path` syntax
23✔
441
                                        fields = append(fields, fieldName)
23✔
442
                                } else if qualifier != "" && qualifier != "kv" {
31✔
443
                                        // Parser split v.address.city into qualifier="address", name="city"
4✔
444
                                        // Reconstruct as dotted path: "address.city"
4✔
445
                                        fields = append(fields, qualifier+"."+fieldName)
4✔
446
                                }
4✔
447
                        }
448
                case *sqlparser.StarExpr:
63✔
449
                        // SELECT * - no specific fields
63✔
450
                        fields = nil
63✔
451
                }
452
        }
453

454
        // Parse LIMIT
455
        if stmt.Limit != nil {
98✔
456
                if stmt.Limit.Rowcount != nil {
8✔
457
                        if val, ok := stmt.Limit.Rowcount.(*sqlparser.SQLVal); ok {
8✔
458
                                if n, err := strconv.Atoi(string(val.Val)); err == nil {
8✔
459
                                        limit = n
4✔
460
                                }
4✔
461
                        }
462
                }
463
        }
464

465
        // Parse WHERE
466
        if stmt.Where != nil {
174✔
467
                s.parseWhere(stmt.Where.Expr, &keyEquals, &keyPrefix, &keyStart, &keyEnd)
80✔
468
        }
80✔
469

470
        count := 0
94✔
471
        isAggregate := len(aggs) > 0
94✔
472

94✔
473
        // Callback for processing each row
94✔
474
        processRow := func(key []byte, val tinykvs.Value) bool {
253✔
475
                if !isAggregate && count >= limit {
161✔
476
                        return false
2✔
477
                }
2✔
478
                if isAggregate {
189✔
479
                        for _, agg := range aggs {
76✔
480
                                agg.update(val)
44✔
481
                        }
44✔
482
                } else {
125✔
483
                        printRowFields(key, val, fields)
125✔
484
                }
125✔
485
                count++
157✔
486
                return true
157✔
487
        }
488

489
        var err error
94✔
490
        if keyEquals != "" {
155✔
491
                // Point lookup
61✔
492
                val, e := s.store.Get([]byte(keyEquals))
61✔
493
                if e == tinykvs.ErrKeyNotFound {
66✔
494
                        if isAggregate {
6✔
495
                                printAggregateResults(aggs)
1✔
496
                        } else {
5✔
497
                                fmt.Println("(0 rows)")
4✔
498
                        }
4✔
499
                        return
5✔
500
                }
501
                if e != nil {
57✔
502
                        fmt.Printf("Error: %v\n", e)
1✔
503
                        return
1✔
504
                }
1✔
505
                processRow([]byte(keyEquals), val)
55✔
506
        } else if keyPrefix != "" {
44✔
507
                err = s.store.ScanPrefix([]byte(keyPrefix), processRow)
11✔
508
        } else if keyStart != "" && keyEnd != "" {
37✔
509
                err = s.store.ScanRange([]byte(keyStart), []byte(keyEnd), processRow)
4✔
510
        } else {
22✔
511
                err = s.store.ScanPrefix(nil, processRow)
18✔
512
        }
18✔
513

514
        if err != nil {
91✔
515
                fmt.Printf("Error: %v\n", err)
3✔
516
                return
3✔
517
        }
3✔
518

519
        if isAggregate {
93✔
520
                printAggregateResults(aggs)
8✔
521
        } else {
85✔
522
                fmt.Printf("(%d rows)\n", count)
77✔
523
        }
77✔
524
}
525

526
func parseAggregateFunc(funcExpr *sqlparser.FuncExpr) *aggregator {
15✔
527
        funcName := strings.ToLower(funcExpr.Name.String())
15✔
528

15✔
529
        var typ aggType
15✔
530
        switch funcName {
15✔
531
        case "count":
4✔
532
                typ = aggCount
4✔
533
        case "sum":
4✔
534
                typ = aggSum
4✔
535
        case "avg":
3✔
536
                typ = aggAvg
3✔
537
        case "min":
2✔
538
                typ = aggMin
2✔
539
        case "max":
2✔
540
                typ = aggMax
2✔
NEW
541
        default:
×
NEW
542
                return nil
×
543
        }
544

545
        agg := &aggregator{typ: typ}
15✔
546

15✔
547
        // Extract field argument for sum/avg/min/max
15✔
548
        if len(funcExpr.Exprs) > 0 {
26✔
549
                if aliased, ok := funcExpr.Exprs[0].(*sqlparser.AliasedExpr); ok {
22✔
550
                        if col, ok := aliased.Expr.(*sqlparser.ColName); ok {
22✔
551
                                qualifier := strings.ToLower(col.Qualifier.Name.String())
11✔
552
                                fieldName := col.Name.String()
11✔
553

11✔
554
                                if qualifier == "v" {
21✔
555
                                        agg.field = fieldName
10✔
556
                                } else if qualifier != "" && qualifier != "kv" {
12✔
557
                                        agg.field = qualifier + "." + fieldName
1✔
558
                                }
1✔
559
                        }
560
                }
561
        }
562

563
        // Build alias for display
564
        if agg.field != "" {
26✔
565
                agg.alias = fmt.Sprintf("%s(v.%s)", funcName, agg.field)
11✔
566
        } else {
15✔
567
                agg.alias = fmt.Sprintf("%s()", funcName)
4✔
568
        }
4✔
569

570
        return agg
15✔
571
}
572

573
func printAggregateResults(aggs []*aggregator) {
9✔
574
        // Print header
9✔
575
        headers := make([]string, len(aggs))
9✔
576
        for i, agg := range aggs {
24✔
577
                headers[i] = agg.alias
15✔
578
        }
15✔
579
        fmt.Println(strings.Join(headers, " | "))
9✔
580

9✔
581
        // Print separator
9✔
582
        seps := make([]string, len(aggs))
9✔
583
        for i, agg := range aggs {
24✔
584
                seps[i] = strings.Repeat("-", len(agg.alias))
15✔
585
        }
15✔
586
        fmt.Println(strings.Join(seps, "-+-"))
9✔
587

9✔
588
        // Print values
9✔
589
        values := make([]string, len(aggs))
9✔
590
        for i, agg := range aggs {
24✔
591
                result := agg.result()
15✔
592
                // Pad to match header width
15✔
593
                if len(result) < len(agg.alias) {
30✔
594
                        result = strings.Repeat(" ", len(agg.alias)-len(result)) + result
15✔
595
                }
15✔
596
                values[i] = result
15✔
597
        }
598
        fmt.Println(strings.Join(values, " | "))
9✔
599
}
600

601
func (s *Shell) parseWhere(expr sqlparser.Expr, keyEquals, keyPrefix, keyStart, keyEnd *string) {
101✔
602
        switch e := expr.(type) {
101✔
603
        case *sqlparser.ComparisonExpr:
93✔
604
                // Check if left side is 'k' column
93✔
605
                if col, ok := e.Left.(*sqlparser.ColName); ok {
186✔
606
                        colName := strings.ToLower(col.Name.String())
93✔
607
                        if colName == "k" {
186✔
608
                                val := extractValue(e.Right)
93✔
609
                                switch e.Operator {
93✔
610
                                case "=":
70✔
611
                                        *keyEquals = val
70✔
612
                                case "like":
16✔
613
                                        // Only support prefix matching (trailing %)
16✔
614
                                        if strings.HasSuffix(val, "%") && !strings.Contains(val[:len(val)-1], "%") {
31✔
615
                                                *keyPrefix = val[:len(val)-1]
15✔
616
                                        } else {
16✔
617
                                                fmt.Println("Warning: LIKE only supports prefix matching (e.g., 'prefix%')")
1✔
618
                                        }
1✔
619
                                case ">=":
4✔
620
                                        *keyStart = val
4✔
621
                                case "<=":
3✔
622
                                        *keyEnd = val
3✔
623
                                }
624
                        }
625
                }
626
        case *sqlparser.RangeCond:
6✔
627
                // BETWEEN
6✔
628
                if col, ok := e.Left.(*sqlparser.ColName); ok {
12✔
629
                        if strings.ToLower(col.Name.String()) == "k" {
12✔
630
                                *keyStart = extractValue(e.From)
6✔
631
                                *keyEnd = extractValue(e.To)
6✔
632
                        }
6✔
633
                }
634
        case *sqlparser.AndExpr:
2✔
635
                s.parseWhere(e.Left, keyEquals, keyPrefix, keyStart, keyEnd)
2✔
636
                s.parseWhere(e.Right, keyEquals, keyPrefix, keyStart, keyEnd)
2✔
637
        }
638
}
639

640
func extractValue(expr sqlparser.Expr) string {
288✔
641
        switch v := expr.(type) {
288✔
642
        case *sqlparser.SQLVal:
288✔
643
                switch v.Type {
288✔
644
                case sqlparser.StrVal:
284✔
645
                        return string(v.Val)
284✔
646
                case sqlparser.HexVal:
2✔
647
                        // x'deadbeef'
2✔
648
                        decoded, _ := hexDecode(string(v.Val))
2✔
649
                        return string(decoded)
2✔
650
                case sqlparser.IntVal:
2✔
651
                        return string(v.Val)
2✔
652
                }
653
        }
NEW
654
        return ""
×
655
}
656

657
func hexDecode(s string) ([]byte, error) {
12✔
658
        // Remove any spaces
12✔
659
        s = strings.ReplaceAll(s, " ", "")
12✔
660
        result := make([]byte, len(s)/2)
12✔
661
        for i := 0; i < len(s)/2; i++ {
83✔
662
                b, err := strconv.ParseUint(s[i*2:i*2+2], 16, 8)
71✔
663
                if err != nil {
72✔
664
                        return nil, err
1✔
665
                }
1✔
666
                result[i] = byte(b)
70✔
667
        }
668
        return result, nil
11✔
669
}
670

671
// isMsgpackMap checks if data starts with a msgpack map marker
672
func isMsgpackMap(data []byte) bool {
13✔
673
        if len(data) == 0 {
14✔
674
                return false
1✔
675
        }
1✔
676
        b := data[0]
12✔
677
        // fixmap (0x80-0x8f), map16 (0xde), map32 (0xdf)
12✔
678
        return (b >= 0x80 && b <= 0x8f) || b == 0xde || b == 0xdf
12✔
679
}
680

681
// extractNestedField extracts a nested field value from a record using a dotted path.
682
// For example, path "address.city" extracts record["address"]["city"].
683
func extractNestedField(record map[string]any, path string) (any, bool) {
69✔
684
        parts := strings.Split(path, ".")
69✔
685
        var current any = record
69✔
686
        for _, part := range parts {
160✔
687
                m, ok := current.(map[string]any)
91✔
688
                if !ok {
92✔
689
                        return nil, false
1✔
690
                }
1✔
691
                current, ok = m[part]
90✔
692
                if !ok {
96✔
693
                        return nil, false
6✔
694
                }
6✔
695
        }
696
        return current, true
62✔
697
}
698

699
// extractValueAndType returns the value string and whether it was a hex value
700
func extractValueAndType(expr sqlparser.Expr) (value string, hexBytes []byte, isHex bool) {
179✔
701
        switch v := expr.(type) {
179✔
702
        case *sqlparser.SQLVal:
179✔
703
                switch v.Type {
179✔
704
                case sqlparser.StrVal:
170✔
705
                        return string(v.Val), nil, false
170✔
706
                case sqlparser.HexVal:
6✔
707
                        decoded, _ := hexDecode(string(v.Val))
6✔
708
                        return string(decoded), decoded, true
6✔
709
                case sqlparser.IntVal:
3✔
710
                        return string(v.Val), nil, false
3✔
711
                }
712
        }
NEW
713
        return "", nil, false
×
714
}
715

716
func (s *Shell) handleInsert(stmt *sqlparser.Insert) {
178✔
717
        // Extract values from INSERT
178✔
718
        rows, ok := stmt.Rows.(sqlparser.Values)
178✔
719
        if !ok || len(rows) == 0 {
178✔
NEW
720
                fmt.Println("Error: Invalid INSERT syntax")
×
NEW
721
                return
×
NEW
722
        }
×
723

724
        inserted := 0
178✔
725
        for _, row := range rows {
358✔
726
                if len(row) < 2 {
181✔
727
                        fmt.Println("Error: INSERT requires (key, value)")
1✔
728
                        continue
1✔
729
                }
730

731
                key := extractValue(row[0])
179✔
732
                value, hexBytes, isHex := extractValueAndType(row[1])
179✔
733

179✔
734
                if key == "" {
180✔
735
                        fmt.Println("Error: key cannot be empty")
1✔
736
                        continue
1✔
737
                }
738

739
                // Try msgpack first if it's hex data that looks like a msgpack map
740
                if isHex && isMsgpackMap(hexBytes) {
182✔
741
                        if record, err := tinykvs.DecodeMsgpack(hexBytes); err == nil {
4✔
NEW
742
                                if err := s.store.PutMap([]byte(key), record); err != nil {
×
NEW
743
                                        fmt.Printf("Error: %v\n", err)
×
NEW
744
                                        continue
×
745
                                }
NEW
746
                                inserted++
×
NEW
747
                                continue
×
748
                        }
749
                        // Fall through to store as bytes if msgpack decode fails
750
                }
751

752
                // Detect JSON and store as record
753
                if strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}") {
220✔
754
                        var record map[string]any
42✔
755
                        if err := json.Unmarshal([]byte(value), &record); err == nil {
83✔
756
                                if err := s.store.PutMap([]byte(key), record); err != nil {
42✔
757
                                        fmt.Printf("Error: %v\n", err)
1✔
758
                                        continue
1✔
759
                                }
760
                                inserted++
40✔
761
                                continue
40✔
762
                        }
763
                }
764

765
                // Store hex data as bytes, strings as strings
766
                if isHex {
143✔
767
                        if err := s.store.PutBytes([]byte(key), hexBytes); err != nil {
8✔
768
                                fmt.Printf("Error: %v\n", err)
2✔
769
                                continue
2✔
770
                        }
771
                } else {
131✔
772
                        if err := s.store.PutString([]byte(key), value); err != nil {
132✔
773
                                fmt.Printf("Error: %v\n", err)
1✔
774
                                continue
1✔
775
                        }
776
                }
777
                inserted++
134✔
778
        }
779

780
        fmt.Printf("INSERT %d\n", inserted)
178✔
781
}
782

783
func (s *Shell) handleUpdate(stmt *sqlparser.Update) {
6✔
784
        // Extract key from WHERE
6✔
785
        var keyEquals string
6✔
786
        if stmt.Where != nil {
11✔
787
                var dummy1, dummy2, dummy3 string
5✔
788
                s.parseWhere(stmt.Where.Expr, &keyEquals, &dummy1, &dummy2, &dummy3)
5✔
789
        }
5✔
790

791
        if keyEquals == "" {
7✔
792
                fmt.Println("Error: UPDATE requires WHERE k = 'value'")
1✔
793
                return
1✔
794
        }
1✔
795

796
        // Extract new value from SET
797
        var newValue string
5✔
798
        for _, expr := range stmt.Exprs {
10✔
799
                if strings.ToLower(expr.Name.Name.String()) == "v" {
9✔
800
                        newValue = extractValue(expr.Expr)
4✔
801
                        break
4✔
802
                }
803
        }
804

805
        if err := s.store.PutString([]byte(keyEquals), newValue); err != nil {
6✔
806
                fmt.Printf("Error: %v\n", err)
1✔
807
                return
1✔
808
        }
1✔
809

810
        fmt.Println("UPDATE 1")
4✔
811
}
812

813
func (s *Shell) handleDelete(stmt *sqlparser.Delete) {
13✔
814
        var keyEquals string
13✔
815
        var keyPrefix string
13✔
816
        var keyStart, keyEnd string
13✔
817

13✔
818
        if stmt.Where != nil {
25✔
819
                s.parseWhere(stmt.Where.Expr, &keyEquals, &keyPrefix, &keyStart, &keyEnd)
12✔
820
        }
12✔
821

822
        if keyEquals != "" {
17✔
823
                // Delete single key
4✔
824
                if err := s.store.Delete([]byte(keyEquals)); err != nil {
5✔
825
                        fmt.Printf("Error: %v\n", err)
1✔
826
                        return
1✔
827
                }
1✔
828
                fmt.Println("DELETE 1")
3✔
829
        } else if keyPrefix != "" {
13✔
830
                // Delete by prefix
4✔
831
                deleted, err := s.store.DeletePrefix([]byte(keyPrefix))
4✔
832
                if err != nil {
5✔
833
                        fmt.Printf("Error: %v\n", err)
1✔
834
                        return
1✔
835
                }
1✔
836
                fmt.Printf("DELETE %d\n", deleted)
3✔
837
        } else if keyStart != "" && keyEnd != "" {
9✔
838
                // Delete range
4✔
839
                deleted, err := s.store.DeleteRange([]byte(keyStart), []byte(keyEnd))
4✔
840
                if err != nil {
5✔
841
                        fmt.Printf("Error: %v\n", err)
1✔
842
                        return
1✔
843
                }
1✔
844
                fmt.Printf("DELETE %d\n", deleted)
3✔
845
        } else {
1✔
846
                fmt.Println("Error: DELETE requires WHERE clause")
1✔
847
        }
1✔
848
}
849

850
func printRowFields(key []byte, val tinykvs.Value, fields []string) {
126✔
851
        keyStr := formatKey(key)
126✔
852
        switch val.Type {
126✔
853
        case tinykvs.ValueTypeInt64:
1✔
854
                fmt.Printf("%s | %d\n", keyStr, val.Int64)
1✔
855
        case tinykvs.ValueTypeFloat64:
1✔
856
                fmt.Printf("%s | %f\n", keyStr, val.Float64)
1✔
857
        case tinykvs.ValueTypeBool:
1✔
858
                fmt.Printf("%s | %t\n", keyStr, val.Bool)
1✔
859
        case tinykvs.ValueTypeString, tinykvs.ValueTypeBytes:
86✔
860
                if len(val.Bytes) > 100 {
87✔
861
                        fmt.Printf("%s | %s... (%d bytes)\n", keyStr, string(val.Bytes[:100]), len(val.Bytes))
1✔
862
                } else {
86✔
863
                        fmt.Printf("%s | %s\n", keyStr, string(val.Bytes))
85✔
864
                }
85✔
865
        case tinykvs.ValueTypeRecord, tinykvs.ValueTypeMsgpack:
36✔
866
                var record map[string]any
36✔
867
                if val.Type == tinykvs.ValueTypeRecord {
39✔
868
                        record = val.Record
3✔
869
                } else {
36✔
870
                        // Decode msgpack to map for display
33✔
871
                        if err := msgpack.Unmarshal(val.Bytes, &record); err != nil {
33✔
NEW
872
                                fmt.Printf("%s | (msgpack decode error)\n", keyStr)
×
NEW
873
                                return
×
NEW
874
                        }
×
875
                }
876
                if record == nil {
37✔
877
                        fmt.Printf("%s | {}\n", keyStr)
1✔
878
                        return
1✔
879
                }
1✔
880
                if len(fields) > 0 {
57✔
881
                        // Print only selected fields (supports nested paths like "address.city")
22✔
882
                        values := make([]string, len(fields))
22✔
883
                        for i, field := range fields {
51✔
884
                                if v, ok := extractNestedField(record, field); ok {
55✔
885
                                        values[i] = fmt.Sprintf("%v", v)
26✔
886
                                } else {
29✔
887
                                        values[i] = "NULL"
3✔
888
                                }
3✔
889
                        }
890
                        fmt.Printf("%s | %s\n", keyStr, strings.Join(values, " | "))
22✔
891
                } else {
13✔
892
                        // Print as JSON
13✔
893
                        jsonBytes, _ := json.Marshal(record)
13✔
894
                        fmt.Printf("%s | %s\n", keyStr, string(jsonBytes))
13✔
895
                }
13✔
896
        default:
1✔
897
                fmt.Printf("%s | (unknown type)\n", keyStr)
1✔
898
        }
899
}
900

901
func (s *Shell) exportCSV(filename string) {
11✔
902
        file, err := os.Create(filename)
11✔
903
        if err != nil {
12✔
904
                fmt.Printf("Error creating file: %v\n", err)
1✔
905
                return
1✔
906
        }
1✔
907
        defer file.Close()
10✔
908

10✔
909
        writer := csv.NewWriter(file)
10✔
910
        defer writer.Flush()
10✔
911

10✔
912
        // Write header - simple format: key, value
10✔
913
        writer.Write([]string{"key", "value"})
10✔
914

10✔
915
        var count int64
10✔
916
        err = s.store.ScanPrefix(nil, func(key []byte, val tinykvs.Value) bool {
20,128✔
917
                var valueStr string
20,118✔
918
                switch val.Type {
20,118✔
919
                case tinykvs.ValueTypeInt64:
2✔
920
                        valueStr = strconv.FormatInt(val.Int64, 10)
2✔
921
                case tinykvs.ValueTypeFloat64:
2✔
922
                        valueStr = strconv.FormatFloat(val.Float64, 'g', -1, 64)
2✔
923
                case tinykvs.ValueTypeBool:
2✔
924
                        valueStr = strconv.FormatBool(val.Bool)
2✔
925
                case tinykvs.ValueTypeString:
20,108✔
926
                        valueStr = string(val.Bytes)
20,108✔
NEW
927
                case tinykvs.ValueTypeBytes:
×
NEW
928
                        // Bytes exported as hex with prefix
×
NEW
929
                        valueStr = "0x" + hex.EncodeToString(val.Bytes)
×
NEW
930
                case tinykvs.ValueTypeRecord:
×
NEW
931
                        jsonBytes, _ := json.Marshal(val.Record)
×
NEW
932
                        valueStr = string(jsonBytes)
×
933
                case tinykvs.ValueTypeMsgpack:
4✔
934
                        var record map[string]any
4✔
935
                        if err := msgpack.Unmarshal(val.Bytes, &record); err != nil {
4✔
NEW
936
                                return true // skip on decode error
×
NEW
937
                        }
×
938
                        jsonBytes, _ := json.Marshal(record)
4✔
939
                        valueStr = string(jsonBytes)
4✔
NEW
940
                default:
×
NEW
941
                        return true // skip unknown types
×
942
                }
943

944
                writer.Write([]string{string(key), valueStr})
20,118✔
945

20,118✔
946
                count++
20,118✔
947
                if count%10000 == 0 {
20,120✔
948
                        fmt.Printf("\rExported %d keys...", count)
2✔
949
                }
2✔
950
                return true
20,118✔
951
        })
952

953
        if err != nil {
11✔
954
                fmt.Printf("\nError scanning: %v\n", err)
1✔
955
                return
1✔
956
        }
1✔
957

958
        fmt.Printf("\rExported %d keys to %s\n", count, filename)
9✔
959
}
960

961
func (s *Shell) importCSV(filename string) {
18✔
962
        file, err := os.Open(filename)
18✔
963
        if err != nil {
19✔
964
                fmt.Printf("Error opening file: %v\n", err)
1✔
965
                return
1✔
966
        }
1✔
967
        defer file.Close()
17✔
968

17✔
969
        reader := csv.NewReader(bufio.NewReader(file))
17✔
970

17✔
971
        // Read header to detect format
17✔
972
        header, err := reader.Read()
17✔
973
        if err != nil {
17✔
NEW
974
                fmt.Printf("Error reading header: %v\n", err)
×
NEW
975
                return
×
NEW
976
        }
×
977

978
        var count int64
17✔
979
        var errors int64
17✔
980

17✔
981
        // Detect format based on header
17✔
982
        // Old format: "key,type,value" (hex-encoded)
17✔
983
        // Simple format: "key,value" (key is string, value is string/JSON)
17✔
984
        // Record format: "key,field1,field2,..." (first col is key, rest are fields)
17✔
985
        //   - supports type hints: "field:type" where type is string/int/float/bool/json
17✔
986
        isOldFormat := len(header) == 3 && header[0] == "key" && header[1] == "type" && header[2] == "value"
17✔
987
        isSimpleFormat := len(header) == 2
17✔
988

17✔
989
        // Parse field names and type hints for record format
17✔
990
        type fieldSpec struct {
17✔
991
                name     string
17✔
992
                typeHint string // "", "string", "int", "float", "bool", "json"
17✔
993
        }
17✔
994
        var fieldSpecs []fieldSpec
17✔
995
        if !isOldFormat && !isSimpleFormat {
19✔
996
                for i := 1; i < len(header); i++ {
10✔
997
                        spec := fieldSpec{}
8✔
998
                        if idx := strings.LastIndex(header[i], ":"); idx != -1 {
13✔
999
                                spec.name = header[i][:idx]
5✔
1000
                                spec.typeHint = strings.ToLower(header[i][idx+1:])
5✔
1001
                        } else {
8✔
1002
                                spec.name = header[i]
3✔
1003
                                spec.typeHint = "" // auto-detect
3✔
1004
                        }
3✔
1005
                        fieldSpecs = append(fieldSpecs, spec)
8✔
1006
                }
1007
        }
1008

1009
        for {
10,073✔
1010
                record, err := reader.Read()
10,056✔
1011
                if err == io.EOF {
10,073✔
1012
                        break
17✔
1013
                }
1014
                if err != nil {
10,042✔
1015
                        errors++
3✔
1016
                        continue
3✔
1017
                }
1018

1019
                var key []byte
10,036✔
1020
                var val tinykvs.Value
10,036✔
1021

10,036✔
1022
                if isOldFormat {
10,045✔
1023
                        // Old format: hex key, type, hex/json value
9✔
1024
                        if len(record) != 3 {
9✔
NEW
1025
                                errors++
×
NEW
1026
                                continue
×
1027
                        }
1028
                        key, err = hex.DecodeString(record[0])
9✔
1029
                        if err != nil {
11✔
1030
                                errors++
2✔
1031
                                continue
2✔
1032
                        }
1033
                        val, err = parseTypedValue(record[1], record[2])
7✔
1034
                        if err != nil {
14✔
1035
                                errors++
7✔
1036
                                continue
7✔
1037
                        }
1038
                } else if isSimpleFormat {
20,049✔
1039
                        // Simple format: string key, string/JSON value
10,022✔
1040
                        if len(record) != 2 {
10,022✔
NEW
1041
                                errors++
×
NEW
1042
                                continue
×
1043
                        }
1044
                        key = []byte(record[0])
10,022✔
1045
                        val = parseAutoValue(record[1])
10,022✔
1046
                } else {
5✔
1047
                        // Record format: first column is key, rest are fields with optional type hints
5✔
1048
                        if len(record) != len(header) {
5✔
NEW
1049
                                errors++
×
NEW
1050
                                continue
×
1051
                        }
1052
                        key = []byte(record[0])
5✔
1053
                        fields := make(map[string]any)
5✔
1054
                        for i, spec := range fieldSpecs {
24✔
1055
                                fields[spec.name] = parseFieldValueWithHint(record[i+1], spec.typeHint)
19✔
1056
                        }
19✔
1057
                        val = tinykvs.RecordValue(fields)
5✔
1058
                }
1059

1060
                if err := s.store.Put(key, val); err != nil {
10,027✔
NEW
1061
                        errors++
×
NEW
1062
                        continue
×
1063
                }
1064
                count++
10,027✔
1065

10,027✔
1066
                if count%10000 == 0 {
10,028✔
1067
                        fmt.Printf("\rImported %d keys...", count)
1✔
1068
                }
1✔
1069
        }
1070

1071
        fmt.Printf("\rImported %d keys (%d errors)\n", count, errors)
17✔
1072
}
1073

1074
// parseTypedValue parses a value in the old export format (type + hex/json encoded value)
1075
func parseTypedValue(typeName, valueStr string) (tinykvs.Value, error) {
7✔
1076
        switch typeName {
7✔
1077
        case "int64":
1✔
1078
                v, err := strconv.ParseInt(valueStr, 10, 64)
1✔
1079
                if err != nil {
2✔
1080
                        return tinykvs.Value{}, err
1✔
1081
                }
1✔
NEW
1082
                return tinykvs.Int64Value(v), nil
×
1083
        case "float64":
1✔
1084
                v, err := strconv.ParseFloat(valueStr, 64)
1✔
1085
                if err != nil {
2✔
1086
                        return tinykvs.Value{}, err
1✔
1087
                }
1✔
NEW
1088
                return tinykvs.Float64Value(v), nil
×
1089
        case "bool":
1✔
1090
                v, err := strconv.ParseBool(valueStr)
1✔
1091
                if err != nil {
2✔
1092
                        return tinykvs.Value{}, err
1✔
1093
                }
1✔
NEW
1094
                return tinykvs.BoolValue(v), nil
×
1095
        case "string":
1✔
1096
                bytes, err := hex.DecodeString(valueStr)
1✔
1097
                if err != nil {
2✔
1098
                        return tinykvs.Value{}, err
1✔
1099
                }
1✔
NEW
1100
                return tinykvs.StringValue(string(bytes)), nil
×
1101
        case "bytes":
1✔
1102
                bytes, err := hex.DecodeString(valueStr)
1✔
1103
                if err != nil {
2✔
1104
                        return tinykvs.Value{}, err
1✔
1105
                }
1✔
NEW
1106
                return tinykvs.BytesValue(bytes), nil
×
1107
        case "record":
1✔
1108
                var record map[string]any
1✔
1109
                if err := json.Unmarshal([]byte(valueStr), &record); err != nil {
2✔
1110
                        return tinykvs.Value{}, err
1✔
1111
                }
1✔
NEW
1112
                return tinykvs.RecordValue(record), nil
×
1113
        default:
1✔
1114
                return tinykvs.Value{}, fmt.Errorf("unknown type: %s", typeName)
1✔
1115
        }
1116
}
1117

1118
// parseAutoValue tries to parse a value, auto-detecting JSON and hex bytes
1119
func parseAutoValue(s string) tinykvs.Value {
10,022✔
1120
        // Try hex bytes (0x prefix)
10,022✔
1121
        if strings.HasPrefix(s, "0x") {
10,022✔
NEW
1122
                if bytes, err := hex.DecodeString(s[2:]); err == nil {
×
NEW
1123
                        return tinykvs.BytesValue(bytes)
×
NEW
1124
                }
×
1125
        }
1126
        // Try JSON object
1127
        if strings.HasPrefix(s, "{") {
10,027✔
1128
                var record map[string]any
5✔
1129
                if err := json.Unmarshal([]byte(s), &record); err == nil {
10✔
1130
                        return tinykvs.RecordValue(record)
5✔
1131
                }
5✔
1132
        }
1133
        // Try int
1134
        if i, err := strconv.ParseInt(s, 10, 64); err == nil {
10,020✔
1135
                return tinykvs.Int64Value(i)
3✔
1136
        }
3✔
1137
        // Try float (only if contains decimal point to avoid int ambiguity)
1138
        if strings.Contains(s, ".") {
10,017✔
1139
                if f, err := strconv.ParseFloat(s, 64); err == nil {
6✔
1140
                        return tinykvs.Float64Value(f)
3✔
1141
                }
3✔
1142
        }
1143
        // Try bool
1144
        if s == "true" {
10,014✔
1145
                return tinykvs.BoolValue(true)
3✔
1146
        }
3✔
1147
        if s == "false" {
10,008✔
NEW
1148
                return tinykvs.BoolValue(false)
×
NEW
1149
        }
×
1150
        // Default to string
1151
        return tinykvs.StringValue(s)
10,008✔
1152
}
1153

1154
// parseFieldValue tries to parse a field value, auto-detecting type
1155
func parseFieldValue(s string) any {
9✔
1156
        // Try int
9✔
1157
        if i, err := strconv.ParseInt(s, 10, 64); err == nil {
12✔
1158
                return i
3✔
1159
        }
3✔
1160
        // Try float
1161
        if f, err := strconv.ParseFloat(s, 64); err == nil {
6✔
NEW
1162
                return f
×
NEW
1163
        }
×
1164
        // Try bool
1165
        if b, err := strconv.ParseBool(s); err == nil {
9✔
1166
                return b
3✔
1167
        }
3✔
1168
        // Try JSON
1169
        if strings.HasPrefix(s, "{") || strings.HasPrefix(s, "[") {
3✔
NEW
1170
                var v any
×
NEW
1171
                if err := json.Unmarshal([]byte(s), &v); err == nil {
×
NEW
1172
                        return v
×
NEW
1173
                }
×
1174
        }
1175
        // Default to string
1176
        return s
3✔
1177
}
1178

1179
// parseFieldValueWithHint parses a field value using an optional type hint
1180
// Type hints: "string", "int", "float", "bool", "json", or "" for auto-detect
1181
func parseFieldValueWithHint(s string, typeHint string) any {
19✔
1182
        switch typeHint {
19✔
1183
        case "string", "str", "s":
2✔
1184
                return s
2✔
1185
        case "int", "integer", "i":
2✔
1186
                if i, err := strconv.ParseInt(s, 10, 64); err == nil {
4✔
1187
                        return i
2✔
1188
                }
2✔
NEW
1189
                return s // fallback to string on parse error
×
1190
        case "float", "f", "double", "number":
2✔
1191
                if f, err := strconv.ParseFloat(s, 64); err == nil {
4✔
1192
                        return f
2✔
1193
                }
2✔
NEW
1194
                return s
×
1195
        case "bool", "boolean", "b":
2✔
1196
                if b, err := strconv.ParseBool(s); err == nil {
4✔
1197
                        return b
2✔
1198
                }
2✔
NEW
1199
                return s
×
1200
        case "json", "j", "object":
2✔
1201
                var v any
2✔
1202
                if err := json.Unmarshal([]byte(s), &v); err == nil {
4✔
1203
                        return v
2✔
1204
                }
2✔
NEW
1205
                return s
×
1206
        default:
9✔
1207
                // Auto-detect
9✔
1208
                return parseFieldValue(s)
9✔
1209
        }
1210
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc