diff --git a/internal/cmd/group_flag.go b/internal/cmd/group_flag.go index fa9f3526..4968ca83 100644 --- a/internal/cmd/group_flag.go +++ b/internal/cmd/group_flag.go @@ -8,6 +8,7 @@ import ( "log" "os" "os/exec" + "regexp" "strconv" "strings" "time" @@ -19,6 +20,15 @@ import ( "github.com/tursodatabase/turso-cli/internal/turso" ) +var validTableName = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) + +func validateCSVTableName(name string) error { + if !validTableName.MatchString(name) { + return fmt.Errorf("invalid --csv-table-name %q: must start with a letter or underscore and contain only letters, digits, and underscores", name) + } + return nil +} + var groupBoolFlag bool func addGroupBoolFlag(cmd *cobra.Command, description string) { @@ -80,6 +90,11 @@ func parseDBSeedFlags(client *turso.Client, isAWS bool, cipher string) (*turso.D if csvTableNameFlag != "" && fromCSVFlag == "" { return nil, errors.New("--from-csv must be used with --csv-table-name") } + if csvTableNameFlag != "" { + if err := validateCSVTableName(csvTableNameFlag); err != nil { + return nil, err + } + } if fromDBFlag != "" { return &turso.DBSeed{Type: "database", Name: fromDBFlag, Timestamp: timestamp}, nil @@ -482,7 +497,7 @@ func importCSVIntoSQLite(tempDB *os.File, csvFile, csvTableName string, separato cmd.Stderr = stdErr if err := cmd.Run(); err != nil { - return fmt.Errorf("could not load csv into new database file: %w: %x", err, stdErr.Bytes()) + return fmt.Errorf("could not load csv into new database file: %w: %s", err, stdErr.String()) } return nil } diff --git a/internal/cmd/group_flag_test.go b/internal/cmd/group_flag_test.go index 4dd207e7..84ad0416 100644 --- a/internal/cmd/group_flag_test.go +++ b/internal/cmd/group_flag_test.go @@ -45,6 +45,37 @@ func createTestDatabase(t *testing.T, sizeBytes int) string { return dbPath } +func TestValidateCSVTableName(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + errContains string + }{ + {name: "simple name", input: "users", wantErr: false}, + {name: "underscore prefix", input: "_table", wantErr: false}, + {name: "with numbers", input: "table123", wantErr: false}, + {name: "mixed valid", input: "my_table_2", wantErr: false}, + {name: "hyphen rejected", input: "some-table", wantErr: true, errContains: "invalid --csv-table-name"}, + {name: "starts with digit", input: "1table", wantErr: true, errContains: "invalid --csv-table-name"}, + {name: "empty string", input: "", wantErr: true, errContains: "invalid --csv-table-name"}, + {name: "space in name", input: "my table", wantErr: true, errContains: "invalid --csv-table-name"}, + {name: "dot in name", input: "my.table", wantErr: true, errContains: "invalid --csv-table-name"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateCSVTableName(tt.input) + if tt.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errContains) + } else { + require.NoError(t, err) + } + }) + } +} + func TestRunQuickCheck(t *testing.T) { if _, err := exec.LookPath("sqlite3"); err != nil { t.Skip("sqlite3 not available, skipping test")