Skip to content

Commit 57fc42e

Browse files
fix: validate --csv-table-name and fix hex-encoded error message
Adds upfront validation that --csv-table-name is a valid unquoted SQL identifier (starts with a letter or underscore, contains only letters, digits, and underscores). This prevents silent data loss on newer sqlite3 (≥3.37) and cryptic hex error output on older versions. Also fixes the stderr format verb in importCSVIntoSQLite from %x (hex) to %s so sqlite3 error messages are human-readable. Closes #810
1 parent 4d5c55f commit 57fc42e

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

internal/cmd/group_flag.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"log"
99
"os"
1010
"os/exec"
11+
"regexp"
1112
"strconv"
1213
"strings"
1314
"time"
@@ -19,6 +20,15 @@ import (
1920
"github.com/tursodatabase/turso-cli/internal/turso"
2021
)
2122

23+
var validTableName = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
24+
25+
func validateCSVTableName(name string) error {
26+
if !validTableName.MatchString(name) {
27+
return fmt.Errorf("invalid --csv-table-name %q: must start with a letter or underscore and contain only letters, digits, and underscores", name)
28+
}
29+
return nil
30+
}
31+
2232
var groupBoolFlag bool
2333

2434
func addGroupBoolFlag(cmd *cobra.Command, description string) {
@@ -80,6 +90,11 @@ func parseDBSeedFlags(client *turso.Client, isAWS bool, cipher string) (*turso.D
8090
if csvTableNameFlag != "" && fromCSVFlag == "" {
8191
return nil, errors.New("--from-csv must be used with --csv-table-name")
8292
}
93+
if csvTableNameFlag != "" {
94+
if err := validateCSVTableName(csvTableNameFlag); err != nil {
95+
return nil, err
96+
}
97+
}
8398

8499
if fromDBFlag != "" {
85100
return &turso.DBSeed{Type: "database", Name: fromDBFlag, Timestamp: timestamp}, nil
@@ -482,7 +497,7 @@ func importCSVIntoSQLite(tempDB *os.File, csvFile, csvTableName string, separato
482497
cmd.Stderr = stdErr
483498

484499
if err := cmd.Run(); err != nil {
485-
return fmt.Errorf("could not load csv into new database file: %w: %x", err, stdErr.Bytes())
500+
return fmt.Errorf("could not load csv into new database file: %w: %s", err, stdErr.String())
486501
}
487502
return nil
488503
}

internal/cmd/group_flag_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,37 @@ func createTestDatabase(t *testing.T, sizeBytes int) string {
4545
return dbPath
4646
}
4747

48+
func TestValidateCSVTableName(t *testing.T) {
49+
tests := []struct {
50+
name string
51+
input string
52+
wantErr bool
53+
errContains string
54+
}{
55+
{name: "simple name", input: "users", wantErr: false},
56+
{name: "underscore prefix", input: "_table", wantErr: false},
57+
{name: "with numbers", input: "table123", wantErr: false},
58+
{name: "mixed valid", input: "my_table_2", wantErr: false},
59+
{name: "hyphen rejected", input: "some-table", wantErr: true, errContains: "invalid --csv-table-name"},
60+
{name: "starts with digit", input: "1table", wantErr: true, errContains: "invalid --csv-table-name"},
61+
{name: "empty string", input: "", wantErr: true, errContains: "invalid --csv-table-name"},
62+
{name: "space in name", input: "my table", wantErr: true, errContains: "invalid --csv-table-name"},
63+
{name: "dot in name", input: "my.table", wantErr: true, errContains: "invalid --csv-table-name"},
64+
}
65+
66+
for _, tt := range tests {
67+
t.Run(tt.name, func(t *testing.T) {
68+
err := validateCSVTableName(tt.input)
69+
if tt.wantErr {
70+
require.Error(t, err)
71+
require.Contains(t, err.Error(), tt.errContains)
72+
} else {
73+
require.NoError(t, err)
74+
}
75+
})
76+
}
77+
}
78+
4879
func TestRunQuickCheck(t *testing.T) {
4980
if _, err := exec.LookPath("sqlite3"); err != nil {
5081
t.Skip("sqlite3 not available, skipping test")

0 commit comments

Comments
 (0)