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

freeeve / tinykvs / 21101309417

17 Jan 2026 09:42PM UTC coverage: 68.627% (-4.3%) from 72.975%
21101309417

push

github

freeeve
feat: add ORDER BY support and box-style table formatting

New features:
- ORDER BY support in SQL queries (ASC/DESC, multiple columns)
- Box-style table formatting with Unicode borders
- GitHub Actions CI for multi-platform releases (linux/darwin/windows × amd64/arm64)
- Build script with ldflags for version embedding

Improvements:
- Split shell.go into logical modules (shell_select.go, shell_write.go,
  shell_csv.go, shell_sql.go, shell_sort.go)
- Version string no longer shows redundant commit hash
- Reorganized tests into corresponding test files
- Added batch_parallel.go for parallel batch operations
- Test coverage improvements

975 of 1677 new or added lines in 11 files covered. (58.14%)

8 existing lines in 3 files now uncovered.

5180 of 7548 relevant lines covered (68.63%)

430934.64 hits per line

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

74.68
/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 {
157✔
26
        // History file in user's home directory
157✔
27
        historyFile := ""
157✔
28
        if home, err := os.UserHomeDir(); err == nil {
314✔
29
                historyFile = filepath.Join(home, ".tinykvs_history")
157✔
30
        }
157✔
31

32
        return &Shell{
157✔
33
                store:       store,
157✔
34
                prompt:      "tinykvs> ",
157✔
35
                historyFile: historyFile,
157✔
36
        }
157✔
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)
×
45

×
46
        // Load history
×
47
        if s.historyFile != "" {
×
48
                if f, err := os.Open(s.historyFile); err == nil {
×
49
                        s.line.ReadHistory(f)
×
50
                        f.Close()
×
51
                }
×
52
        }
53

NEW
54
        fmt.Println("TinyKVS Shell " + versionString())
×
55
        fmt.Println("Type \\help for help, \\q to quit")
×
56
        fmt.Println()
×
57

×
58
        for {
×
59
                input, err := s.line.Prompt(s.prompt)
×
60
                if err != nil {
×
61
                        if err == liner.ErrPromptAborted {
×
62
                                fmt.Println("^C")
×
63
                                continue
×
64
                        }
65
                        // Ctrl+D (EOF) or other error - exit gracefully
NEW
66
                        fmt.Println()
×
NEW
67
                        break
×
68
                }
69

70
                input = strings.TrimSpace(input)
×
71
                if input == "" {
×
72
                        continue
×
73
                }
74

75
                // Add to history
76
                s.line.AppendHistory(input)
×
77

×
78
                if !s.execute(input) {
×
79
                        break
×
80
                }
81
        }
82

83
        // Save history
84
        if s.historyFile != "" {
×
85
                if f, err := os.Create(s.historyFile); err == nil {
×
86
                        s.line.WriteHistory(f)
×
87
                        f.Close()
×
88
                }
×
89
        }
90
}
91

92
// execute runs a command. Returns false to exit.
93
func (s *Shell) execute(line string) bool {
350✔
94
        // Handle shell commands
350✔
95
        if strings.HasPrefix(line, "\\") {
408✔
96
                return s.handleCommand(line)
58✔
97
        }
58✔
98

99
        // Remove trailing semicolon if present
100
        line = strings.TrimSuffix(line, ";")
292✔
101

292✔
102
        // Pre-process SQL functions (uint64_be, byte, fnv64, etc.) and concatenation
292✔
103
        line = preprocessFunctions(line)
292✔
104

292✔
105
        // Pre-process: convert "STARTS WITH x'...'" to "LIKE '$$HEX$$...%'"
292✔
106
        // and "STARTS WITH '...'" to "LIKE '...%'"
292✔
107
        line = preprocessStartsWith(line)
292✔
108

292✔
109
        // Extract ORDER BY before SQL parsing (parser doesn't support ORDER BY for kv)
292✔
110
        line, orderBy := ParseOrderBy(line)
292✔
111

292✔
112
        // Parse SQL
292✔
113
        stmt, err := sqlparser.Parse(line)
292✔
114
        if err != nil {
293✔
115
                fmt.Printf("Parse error: %v\n", err)
1✔
116
                return true
1✔
117
        }
1✔
118

119
        switch st := stmt.(type) {
291✔
120
        case *sqlparser.Select:
94✔
121
                s.handleSelect(st, orderBy)
94✔
122
        case *sqlparser.Insert:
178✔
123
                s.handleInsert(st)
178✔
124
        case *sqlparser.Update:
6✔
125
                s.handleUpdate(st)
6✔
126
        case *sqlparser.Delete:
13✔
127
                s.handleDelete(st)
13✔
128
        default:
×
129
                fmt.Printf("Unsupported statement type: %T\n", stmt)
×
130
        }
131

132
        return true
291✔
133
}
134

135
func (s *Shell) handleCommand(cmd string) bool {
58✔
136
        parts := strings.Fields(cmd)
58✔
137
        if len(parts) == 0 {
58✔
138
                return true
×
139
        }
×
140

141
        switch parts[0] {
58✔
142
        case "\\q", "\\quit", "\\exit":
6✔
143
                fmt.Println("Bye")
6✔
144
                return false
6✔
145
        case "\\help", "\\h", "\\?":
3✔
146
                s.printHelp()
3✔
147
        case "\\stats":
3✔
148
                s.printStats()
3✔
149
        case "\\compact":
3✔
150
                fmt.Println("Compacting...")
3✔
151
                if err := s.store.Compact(); err != nil {
4✔
152
                        fmt.Printf("Error: %v\n", err)
1✔
153
                } else {
3✔
154
                        fmt.Println("Done")
2✔
155
                }
2✔
156
        case "\\flush":
8✔
157
                if err := s.store.Flush(); err != nil {
9✔
158
                        fmt.Printf("Error: %v\n", err)
1✔
159
                } else {
8✔
160
                        fmt.Println("Flushed")
7✔
161
                }
7✔
162
        case "\\tables":
2✔
163
                fmt.Println("Table: kv (k TEXT, v TEXT)")
2✔
164
                fmt.Println("  - k: the key (string or hex with x'...')")
2✔
165
                fmt.Println("  - v: the value")
2✔
166
        case "\\export":
12✔
167
                if len(parts) < 2 {
13✔
168
                        fmt.Println("Usage: \\export <filename.csv>")
1✔
169
                        return true
1✔
170
                }
1✔
171
                s.exportCSV(parts[1])
11✔
172
        case "\\import":
19✔
173
                if len(parts) < 2 {
20✔
174
                        fmt.Println("Usage: \\import <filename.csv>")
1✔
175
                        return true
1✔
176
                }
1✔
177
                s.importCSV(parts[1])
18✔
178
        default:
2✔
179
                fmt.Printf("Unknown command: %s\n", parts[0])
2✔
180
                fmt.Println("Type \\help for help")
2✔
181
        }
182
        return true
50✔
183
}
184

185
func (s *Shell) printHelp() {
3✔
186
        fmt.Println(`SQL Commands:
3✔
187
  SELECT * FROM kv WHERE k = 'mykey'
3✔
188
  SELECT * FROM kv WHERE k LIKE 'prefix%'
3✔
189
  SELECT * FROM kv WHERE k BETWEEN 'a' AND 'z' LIMIT 10
3✔
190
  SELECT * FROM kv LIMIT 100
3✔
191
  SELECT v.name, v.age FROM kv WHERE k = 'user:1'    -- record fields
3✔
192
  SELECT v.address.city FROM kv WHERE k = 'user:1'   -- nested fields
3✔
193

3✔
194
  ORDER BY (buffers results for sorting):
3✔
195
  SELECT * FROM kv ORDER BY k DESC LIMIT 10
3✔
196
  SELECT v.name, v.age FROM kv ORDER BY v.age DESC, v.name
3✔
197
  SELECT * FROM kv WHERE k LIKE 'user:%' ORDER BY v.score LIMIT 100
3✔
198

3✔
199
  Aggregations (streaming):
3✔
200
  SELECT count() FROM kv
3✔
201
  SELECT count(), sum(v.age), avg(v.age) FROM kv
3✔
202
  SELECT min(v.score), max(v.score) FROM kv WHERE k LIKE 'user:%'
3✔
203

3✔
204
  INSERT INTO kv (k, v) VALUES ('mykey', 'myvalue')
3✔
205
  INSERT INTO kv VALUES ('mykey', 'myvalue')
3✔
206
  INSERT INTO kv VALUES ('user:1', '{"name":"Alice","age":30}')  -- JSON record
3✔
207
  INSERT INTO kv VALUES ('user:2', x'82a46e616d65a3426f62a361676514')  -- msgpack
3✔
208

3✔
209
  UPDATE kv SET v = 'newvalue' WHERE k = 'mykey'
3✔
210

3✔
211
  DELETE FROM kv WHERE k = 'mykey'
3✔
212
  DELETE FROM kv WHERE k LIKE 'prefix%'
3✔
213

3✔
214
Shell Commands:
3✔
215
  \help, \h, \?      Show this help
3✔
216
  \stats             Show store statistics
3✔
217
  \compact           Run compaction
3✔
218
  \flush             Flush memtable to disk
3✔
219
  \tables            Show table schema
3✔
220
  \export <file>     Export to CSV (key,value format)
3✔
221
  \import <file>     Import from CSV (auto-detects format)
3✔
222
  \q, \quit          Exit shell
3✔
223

3✔
224
CSV Import Formats:
3✔
225
  key,value              2 columns: key + value (auto-detects type)
3✔
226
  key,col1,col2,...      3+ columns: key + fields become a record
3✔
227
  key,name:string,age:int  Type hints: string, int, float, bool, json
3✔
228

3✔
229
Notes:
3✔
230
  - Use single quotes for strings: 'mykey'
3✔
231
  - Use x'...' for hex values: x'deadbeef'
3✔
232
  - JSON strings are auto-detected and stored as records
3✔
233
  - Hex values starting with msgpack map markers are stored as records
3✔
234
  - Access record fields with v.fieldname in SELECT
3✔
235
  - Nested fields: v.a.b (2 levels) or v.` + "`a.b.c`" + ` (deeper)
3✔
236
  - LIKE only supports prefix matching (trailing %)
3✔
237
  - All operations are on the virtual 'kv' table
3✔
238
  - Use up/down arrows to navigate command history`)
3✔
239
}
3✔
240

241
func (s *Shell) printStats() {
3✔
242
        stats := s.store.Stats()
3✔
243

3✔
244
        fmt.Printf("Memtable: %d keys, %s\n", stats.MemtableCount, formatBytes(stats.MemtableSize))
3✔
245
        fmt.Printf("Cache: %d entries, %s (%.1f%% hit rate)\n",
3✔
246
                stats.CacheStats.Entries, formatBytes(stats.CacheStats.Size),
3✔
247
                cacheHitRate(stats.CacheStats))
3✔
248

3✔
249
        var totalKeys uint64
3✔
250
        var totalSize int64
3✔
251
        for _, level := range stats.Levels {
24✔
252
                if level.NumTables > 0 {
22✔
253
                        fmt.Printf("L%d: %d tables, %d keys, %s\n",
1✔
254
                                level.Level, level.NumTables, level.NumKeys, formatBytes(level.Size))
1✔
255
                        totalKeys += level.NumKeys
1✔
256
                        totalSize += level.Size
1✔
257
                }
1✔
258
        }
259
        fmt.Printf("Total: %d keys, %s\n", totalKeys, formatBytes(totalSize))
3✔
260
}
261

262
func cacheHitRate(cs tinykvs.CacheStats) float64 {
5✔
263
        total := cs.Hits + cs.Misses
5✔
264
        if total == 0 {
9✔
265
                return 0
4✔
266
        }
4✔
267
        return float64(cs.Hits) / float64(total) * 100
1✔
268
}
269

270
// formatIntCommas formats an integer with comma separators for readability.
271
func formatIntCommas(n int64) string {
158✔
272
        if n < 0 {
158✔
NEW
273
                return "-" + formatIntCommas(-n)
×
UNCOV
274
        }
×
275
        s := strconv.FormatInt(n, 10)
158✔
276
        if len(s) <= 3 {
316✔
277
                return s
158✔
278
        }
158✔
279
        // Insert commas from right to left
NEW
280
        var result strings.Builder
×
NEW
281
        for i, c := range s {
×
NEW
282
                if i > 0 && (len(s)-i)%3 == 0 {
×
NEW
283
                        result.WriteByte(',')
×
UNCOV
284
                }
×
NEW
285
                result.WriteRune(c)
×
286
        }
NEW
287
        return result.String()
×
288
}
289

290
// formatDuration formats a duration in human-readable form (e.g., "3m 7s", "2.3s").
291
func formatDuration(d time.Duration) string {
79✔
292
        if d < time.Second {
158✔
293
                return fmt.Sprintf("%dms", d.Milliseconds())
79✔
294
        }
79✔
NEW
295
        if d < time.Minute {
×
NEW
296
                return fmt.Sprintf("%.1fs", d.Seconds())
×
UNCOV
297
        }
×
NEW
298
        minutes := int(d.Minutes())
×
NEW
299
        seconds := int(d.Seconds()) % 60
×
NEW
300
        if minutes < 60 {
×
NEW
301
                return fmt.Sprintf("%dm %ds", minutes, seconds)
×
UNCOV
302
        }
×
NEW
303
        hours := minutes / 60
×
NEW
304
        minutes = minutes % 60
×
NEW
305
        return fmt.Sprintf("%dh %dm %ds", hours, minutes, seconds)
×
306
}
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