• 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

83.66
/cmd/tinykvs/shell_csv.go
1
package main
2

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

14
        "github.com/freeeve/tinykvs"
15
        "github.com/vmihailenco/msgpack/v5"
16
)
17

18
func (s *Shell) exportCSV(filename string) {
11✔
19
        file, err := os.Create(filename)
11✔
20
        if err != nil {
12✔
21
                fmt.Printf("Error creating file: %v\n", err)
1✔
22
                return
1✔
23
        }
1✔
24
        defer file.Close()
10✔
25

10✔
26
        writer := csv.NewWriter(file)
10✔
27
        defer writer.Flush()
10✔
28

10✔
29
        // Write header - simple format: key, value
10✔
30
        writer.Write([]string{"key", "value"})
10✔
31

10✔
32
        var count int64
10✔
33
        err = s.store.ScanPrefix(nil, func(key []byte, val tinykvs.Value) bool {
20,128✔
34
                var valueStr string
20,118✔
35
                switch val.Type {
20,118✔
36
                case tinykvs.ValueTypeInt64:
2✔
37
                        valueStr = strconv.FormatInt(val.Int64, 10)
2✔
38
                case tinykvs.ValueTypeFloat64:
2✔
39
                        valueStr = strconv.FormatFloat(val.Float64, 'g', -1, 64)
2✔
40
                case tinykvs.ValueTypeBool:
2✔
41
                        valueStr = strconv.FormatBool(val.Bool)
2✔
42
                case tinykvs.ValueTypeString:
20,108✔
43
                        valueStr = string(val.Bytes)
20,108✔
NEW
44
                case tinykvs.ValueTypeBytes:
×
NEW
45
                        // Bytes exported as hex with prefix
×
NEW
46
                        valueStr = "0x" + hex.EncodeToString(val.Bytes)
×
NEW
47
                case tinykvs.ValueTypeRecord:
×
NEW
48
                        jsonBytes, _ := json.Marshal(val.Record)
×
NEW
49
                        valueStr = string(jsonBytes)
×
50
                case tinykvs.ValueTypeMsgpack:
4✔
51
                        var record map[string]any
4✔
52
                        if err := msgpack.Unmarshal(val.Bytes, &record); err != nil {
4✔
NEW
53
                                return true // skip on decode error
×
NEW
54
                        }
×
55
                        jsonBytes, _ := json.Marshal(record)
4✔
56
                        valueStr = string(jsonBytes)
4✔
NEW
57
                default:
×
NEW
58
                        return true // skip unknown types
×
59
                }
60

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

20,118✔
63
                count++
20,118✔
64
                if count%10000 == 0 {
20,120✔
65
                        fmt.Printf("\rExported %d keys...", count)
2✔
66
                }
2✔
67
                return true
20,118✔
68
        })
69

70
        if err != nil {
11✔
71
                fmt.Printf("\nError scanning: %v\n", err)
1✔
72
                return
1✔
73
        }
1✔
74

75
        fmt.Printf("\rExported %d keys to %s\n", count, filename)
9✔
76
}
77

78
func (s *Shell) importCSV(filename string) {
18✔
79
        file, err := os.Open(filename)
18✔
80
        if err != nil {
19✔
81
                fmt.Printf("Error opening file: %v\n", err)
1✔
82
                return
1✔
83
        }
1✔
84
        defer file.Close()
17✔
85

17✔
86
        reader := csv.NewReader(bufio.NewReader(file))
17✔
87

17✔
88
        // Read header to detect format
17✔
89
        header, err := reader.Read()
17✔
90
        if err != nil {
17✔
NEW
91
                fmt.Printf("Error reading header: %v\n", err)
×
NEW
92
                return
×
NEW
93
        }
×
94

95
        var count int64
17✔
96
        var errors int64
17✔
97

17✔
98
        // Detect format based on header
17✔
99
        // Old format: "key,type,value" (hex-encoded)
17✔
100
        // Simple format: "key,value" (key is string, value is string/JSON)
17✔
101
        // Record format: "key,field1,field2,..." (first col is key, rest are fields)
17✔
102
        //   - supports type hints: "field:type" where type is string/int/float/bool/json
17✔
103
        isOldFormat := len(header) == 3 && header[0] == "key" && header[1] == "type" && header[2] == "value"
17✔
104
        isSimpleFormat := len(header) == 2
17✔
105

17✔
106
        // Parse field names and type hints for record format
17✔
107
        type fieldSpec struct {
17✔
108
                name     string
17✔
109
                typeHint string // "", "string", "int", "float", "bool", "json"
17✔
110
        }
17✔
111
        var fieldSpecs []fieldSpec
17✔
112
        if !isOldFormat && !isSimpleFormat {
19✔
113
                for i := 1; i < len(header); i++ {
10✔
114
                        spec := fieldSpec{}
8✔
115
                        if idx := strings.LastIndex(header[i], ":"); idx != -1 {
13✔
116
                                spec.name = header[i][:idx]
5✔
117
                                spec.typeHint = strings.ToLower(header[i][idx+1:])
5✔
118
                        } else {
8✔
119
                                spec.name = header[i]
3✔
120
                                spec.typeHint = "" // auto-detect
3✔
121
                        }
3✔
122
                        fieldSpecs = append(fieldSpecs, spec)
8✔
123
                }
124
        }
125

126
        for {
10,073✔
127
                record, err := reader.Read()
10,056✔
128
                if err == io.EOF {
10,073✔
129
                        break
17✔
130
                }
131
                if err != nil {
10,042✔
132
                        errors++
3✔
133
                        continue
3✔
134
                }
135

136
                var key []byte
10,036✔
137
                var val tinykvs.Value
10,036✔
138

10,036✔
139
                if isOldFormat {
10,045✔
140
                        // Old format: hex key, type, hex/json value
9✔
141
                        if len(record) != 3 {
9✔
NEW
142
                                errors++
×
NEW
143
                                continue
×
144
                        }
145
                        key, err = hex.DecodeString(record[0])
9✔
146
                        if err != nil {
11✔
147
                                errors++
2✔
148
                                continue
2✔
149
                        }
150
                        val, err = parseTypedValue(record[1], record[2])
7✔
151
                        if err != nil {
14✔
152
                                errors++
7✔
153
                                continue
7✔
154
                        }
155
                } else if isSimpleFormat {
20,049✔
156
                        // Simple format: string key, string/JSON value
10,022✔
157
                        if len(record) != 2 {
10,022✔
NEW
158
                                errors++
×
NEW
159
                                continue
×
160
                        }
161
                        key = []byte(record[0])
10,022✔
162
                        val = parseAutoValue(record[1])
10,022✔
163
                } else {
5✔
164
                        // Record format: first column is key, rest are fields with optional type hints
5✔
165
                        if len(record) != len(header) {
5✔
NEW
166
                                errors++
×
NEW
167
                                continue
×
168
                        }
169
                        key = []byte(record[0])
5✔
170
                        fields := make(map[string]any)
5✔
171
                        for i, spec := range fieldSpecs {
24✔
172
                                fields[spec.name] = parseFieldValueWithHint(record[i+1], spec.typeHint)
19✔
173
                        }
19✔
174
                        val = tinykvs.RecordValue(fields)
5✔
175
                }
176

177
                if err := s.store.Put(key, val); err != nil {
10,027✔
NEW
178
                        errors++
×
NEW
179
                        continue
×
180
                }
181
                count++
10,027✔
182

10,027✔
183
                if count%10000 == 0 {
10,028✔
184
                        fmt.Printf("\rImported %d keys...", count)
1✔
185
                }
1✔
186
        }
187

188
        fmt.Printf("\rImported %d keys (%d errors)\n", count, errors)
17✔
189
}
190

191
// parseTypedValue parses a value in the old export format (type + hex/json encoded value)
192
func parseTypedValue(typeName, valueStr string) (tinykvs.Value, error) {
7✔
193
        switch typeName {
7✔
194
        case "int64":
1✔
195
                v, err := strconv.ParseInt(valueStr, 10, 64)
1✔
196
                if err != nil {
2✔
197
                        return tinykvs.Value{}, err
1✔
198
                }
1✔
NEW
199
                return tinykvs.Int64Value(v), nil
×
200
        case "float64":
1✔
201
                v, err := strconv.ParseFloat(valueStr, 64)
1✔
202
                if err != nil {
2✔
203
                        return tinykvs.Value{}, err
1✔
204
                }
1✔
NEW
205
                return tinykvs.Float64Value(v), nil
×
206
        case "bool":
1✔
207
                v, err := strconv.ParseBool(valueStr)
1✔
208
                if err != nil {
2✔
209
                        return tinykvs.Value{}, err
1✔
210
                }
1✔
NEW
211
                return tinykvs.BoolValue(v), nil
×
212
        case "string":
1✔
213
                bytes, err := hex.DecodeString(valueStr)
1✔
214
                if err != nil {
2✔
215
                        return tinykvs.Value{}, err
1✔
216
                }
1✔
NEW
217
                return tinykvs.StringValue(string(bytes)), nil
×
218
        case "bytes":
1✔
219
                bytes, err := hex.DecodeString(valueStr)
1✔
220
                if err != nil {
2✔
221
                        return tinykvs.Value{}, err
1✔
222
                }
1✔
NEW
223
                return tinykvs.BytesValue(bytes), nil
×
224
        case "record":
1✔
225
                var record map[string]any
1✔
226
                if err := json.Unmarshal([]byte(valueStr), &record); err != nil {
2✔
227
                        return tinykvs.Value{}, err
1✔
228
                }
1✔
NEW
229
                return tinykvs.RecordValue(record), nil
×
230
        default:
1✔
231
                return tinykvs.Value{}, fmt.Errorf("unknown type: %s", typeName)
1✔
232
        }
233
}
234

235
// parseAutoValue tries to parse a value, auto-detecting JSON and hex bytes
236
func parseAutoValue(s string) tinykvs.Value {
10,022✔
237
        // Try hex bytes (0x prefix)
10,022✔
238
        if strings.HasPrefix(s, "0x") {
10,022✔
NEW
239
                if bytes, err := hex.DecodeString(s[2:]); err == nil {
×
NEW
240
                        return tinykvs.BytesValue(bytes)
×
NEW
241
                }
×
242
        }
243
        // Try JSON object
244
        if strings.HasPrefix(s, "{") {
10,027✔
245
                var record map[string]any
5✔
246
                if err := json.Unmarshal([]byte(s), &record); err == nil {
10✔
247
                        return tinykvs.RecordValue(record)
5✔
248
                }
5✔
249
        }
250
        // Try int
251
        if i, err := strconv.ParseInt(s, 10, 64); err == nil {
10,020✔
252
                return tinykvs.Int64Value(i)
3✔
253
        }
3✔
254
        // Try float (only if contains decimal point to avoid int ambiguity)
255
        if strings.Contains(s, ".") {
10,017✔
256
                if f, err := strconv.ParseFloat(s, 64); err == nil {
6✔
257
                        return tinykvs.Float64Value(f)
3✔
258
                }
3✔
259
        }
260
        // Try bool
261
        if s == "true" {
10,014✔
262
                return tinykvs.BoolValue(true)
3✔
263
        }
3✔
264
        if s == "false" {
10,008✔
NEW
265
                return tinykvs.BoolValue(false)
×
NEW
266
        }
×
267
        // Default to string
268
        return tinykvs.StringValue(s)
10,008✔
269
}
270

271
// parseFieldValue tries to parse a field value, auto-detecting type
272
func parseFieldValue(s string) any {
9✔
273
        // Try int
9✔
274
        if i, err := strconv.ParseInt(s, 10, 64); err == nil {
12✔
275
                return i
3✔
276
        }
3✔
277
        // Try float
278
        if f, err := strconv.ParseFloat(s, 64); err == nil {
6✔
NEW
279
                return f
×
NEW
280
        }
×
281
        // Try bool
282
        if b, err := strconv.ParseBool(s); err == nil {
9✔
283
                return b
3✔
284
        }
3✔
285
        // Try JSON
286
        if strings.HasPrefix(s, "{") || strings.HasPrefix(s, "[") {
3✔
NEW
287
                var v any
×
NEW
288
                if err := json.Unmarshal([]byte(s), &v); err == nil {
×
NEW
289
                        return v
×
NEW
290
                }
×
291
        }
292
        // Default to string
293
        return s
3✔
294
}
295

296
// parseFieldValueWithHint parses a field value using an optional type hint
297
// Type hints: "string", "int", "float", "bool", "json", or "" for auto-detect
298
func parseFieldValueWithHint(s string, typeHint string) any {
19✔
299
        switch typeHint {
19✔
300
        case "string", "str", "s":
2✔
301
                return s
2✔
302
        case "int", "integer", "i":
2✔
303
                if i, err := strconv.ParseInt(s, 10, 64); err == nil {
4✔
304
                        return i
2✔
305
                }
2✔
NEW
306
                return s // fallback to string on parse error
×
307
        case "float", "f", "double", "number":
2✔
308
                if f, err := strconv.ParseFloat(s, 64); err == nil {
4✔
309
                        return f
2✔
310
                }
2✔
NEW
311
                return s
×
312
        case "bool", "boolean", "b":
2✔
313
                if b, err := strconv.ParseBool(s); err == nil {
4✔
314
                        return b
2✔
315
                }
2✔
NEW
316
                return s
×
317
        case "json", "j", "object":
2✔
318
                var v any
2✔
319
                if err := json.Unmarshal([]byte(s), &v); err == nil {
4✔
320
                        return v
2✔
321
                }
2✔
NEW
322
                return s
×
323
        default:
9✔
324
                // Auto-detect
9✔
325
                return parseFieldValue(s)
9✔
326
        }
327
}
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