Skip to content

Commit 2301572

Browse files
authored
Functional tests for CSV format on import command (#1139)
* Adding GCS dataset for CSV functional testing * Updating unit tests to conform to Spanner table naming convention * Added support for running CSV func tests against the PG Dialect
1 parent 6e6f7e2 commit 2301572

File tree

6 files changed

+264
-192
lines changed

6 files changed

+264
-192
lines changed

cmd/import.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"path/filepath"
2525
"strings"
2626
"time"
27+
"unicode"
2728

2829
"github.com/GoogleCloudPlatform/spanner-migration-tool/file_reader"
2930

@@ -265,7 +266,7 @@ This method does not handle validation. It is supposed to be called only after c
265266
*/
266267
func handleTableNameDefaults(tableName, sourceUri string) string {
267268
if len(tableName) != 0 {
268-
return tableName
269+
return sanitizeTableName(tableName)
269270
}
270271

271272
parsedURL, _ := url.Parse(sourceUri)
@@ -277,7 +278,25 @@ func handleTableNameDefaults(tableName, sourceUri string) string {
277278
basePath := filepath.Base(path)
278279

279280
// pick the substring before the first dot
280-
return strings.Split(basePath, ".")[0]
281+
return sanitizeTableName(strings.Split(basePath, ".")[0])
282+
283+
}
284+
285+
func sanitizeTableName(tableName string) string {
286+
tableName = strings.ToLower(tableName)
287+
underscoreOrAlphabet := func(r rune) bool {
288+
return !(unicode.IsLetter(r) || r == '_')
289+
}
290+
291+
underscoreOrAlphanumeric := func(r rune) rune {
292+
if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' {
293+
return r
294+
}
295+
return -1
296+
}
297+
298+
trimmedTableName := strings.TrimLeftFunc(tableName, underscoreOrAlphabet)
299+
return strings.Map(underscoreOrAlphanumeric, trimmedTableName)
281300
}
282301

283302
func init() {

cmd/import_test.go

Lines changed: 104 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -141,39 +141,111 @@ func TestHandleTableNameDefaults_TableNamePresent(t *testing.T) {
141141
assert.Equal(t, "explicit_table", result)
142142
}
143143

144-
func TestHandleTableNameDefaults_TableNameEmptyFileScheme(t *testing.T) {
145-
tableName := ""
146-
sourceUri := "file:///path/to/my_data.csv"
147-
result := handleTableNameDefaults(tableName, sourceUri)
148-
assert.Equal(t, "my_data", result)
149-
}
150-
151-
func TestHandleTableNameDefaults_TableNameEmptyGCScheme(t *testing.T) {
152-
tableName := ""
153-
sourceUri := "gs://my-bucket/data_file.txt"
154-
result := handleTableNameDefaults(tableName, sourceUri)
155-
assert.Equal(t, "data_file", result)
156-
}
157-
158-
func TestHandleTableNameDefaults_TableNameEmptyLocalPathNoScheme(t *testing.T) {
159-
tableName := ""
160-
sourceUri := "/tmp/another_file.json"
161-
result := handleTableNameDefaults(tableName, sourceUri)
162-
assert.Equal(t, "another_file", result)
144+
func TestHandleTableNameDefaults(t *testing.T) {
145+
tests := []struct {
146+
name string
147+
sourceUri string
148+
expected string
149+
}{
150+
{
151+
name: "URIWithTrailingSlash",
152+
sourceUri: "gs://my-bucket/folder/",
153+
expected: "folder",
154+
},
155+
{
156+
name: "RelativePath",
157+
sourceUri: "relative/path/some_data.avro",
158+
expected: "some_data",
159+
},
160+
{
161+
name: "LocalPathNoScheme",
162+
sourceUri: "/tmp/another_file.json",
163+
expected: "another_file",
164+
},
165+
{
166+
name: "GCScheme",
167+
sourceUri: "s://my-bucket/data_file.txt",
168+
expected: "data_file",
169+
},
170+
{
171+
name: "FileScheme",
172+
sourceUri: "file:///path/to/my_data.csv",
173+
expected: "my_data",
174+
},
175+
}
176+
for _, tc := range tests {
177+
t.Run(tc.name, func(t *testing.T) {
178+
result := handleTableNameDefaults("", tc.sourceUri)
179+
assert.Equal(t, tc.expected, result)
180+
})
181+
}
163182
}
164183

165-
func TestHandleTableNameDefaults_TableNameEmptyRelativePath(t *testing.T) {
166-
tableName := ""
167-
sourceUri := "relative/path/some_data.avro"
168-
result := handleTableNameDefaults(tableName, sourceUri)
169-
assert.Equal(t, "some_data", result)
170-
}
184+
func TestSanitizeTableName(t *testing.T) {
185+
tests := []struct {
186+
input string
187+
expected string
188+
}{
189+
// --- Basic Valid Cases ---
190+
{"myTableName", "mytablename"},
191+
{"another_table", "another_table"},
192+
{"table123", "table123"},
193+
{"_leading_underscore", "_leading_underscore"},
194+
{"has_numbers_123", "has_numbers_123"},
195+
{"ALLCAPS", "allcaps"},
196+
197+
// --- Leading Character Trimming (underscoreOrAlphabet) ---
198+
{"_ABC", "_abc"},
199+
{"-table", "table"},
200+
{"#table", "table"},
201+
{"1table", "table"},
202+
{"-1table", "table"},
203+
{" leading_spaces", "leading_spaces"},
204+
{"_leading_underscores_and_spaces", "_leading_underscores_and_spaces"},
205+
{"__leading_double_underscore", "__leading_double_underscore"},
206+
207+
// --- Invalid Characters Removal (underscoreOrAlphanumeric) ---
208+
{"table name", "tablename"},
209+
{"table.name", "tablename"},
210+
{"table-name", "tablename"},
211+
{"table!@#$%^&*()", "table"},
212+
{"table_name_with_spaces and stuff", "table_name_with_spacesandstuff"},
213+
{"mixed_Case_AND_SYMBOLS!@", "mixed_case_and_symbols"},
214+
{"__Table__Name__", "__table__name__"},
215+
{"Table Name With Space And Special Chars!@#$", "tablenamewithspaceandspecialchars"},
216+
217+
// --- Empty/Edge Cases ---
218+
{"", ""},
219+
{" ", ""},
220+
{"!!!", ""},
221+
{"_!@#", "_"},
222+
{"_123", "_123"},
223+
{"__", "__"},
224+
{"A", "a"},
225+
{"1", ""},
226+
{"-", ""},
227+
{"-1", ""},
228+
{"-a", "a"},
229+
230+
// --- Unicode Characters ---
231+
{"tābļē_ňāmē", "tābļē_ňāmē"},
232+
{"table_名稱", "table_名稱"},
233+
{"table_привет", "table_привет"},
234+
{"😊table😁name", "tablename"},
235+
{"table_日本語_123", "table_日本語_123"},
236+
{"你好_world", "你好_world"},
237+
{"_hello_世界_123", "_hello_世界_123"},
238+
{"table_!@#_name", "table__name"},
239+
}
171240

172-
func TestHandleTableNameDefaults_TableNameEmptyURIWithTrailingSlash(t *testing.T) {
173-
tableName := ""
174-
sourceUri := "gs://my-bucket/folder/"
175-
result := handleTableNameDefaults(tableName, sourceUri)
176-
assert.Equal(t, "folder", result)
241+
for _, tt := range tests {
242+
t.Run(tt.input, func(t *testing.T) { // Use t.Run for better test output for each case
243+
got := sanitizeTableName(tt.input)
244+
if got != tt.expected {
245+
t.Errorf("sanitizeTableName(%q) = %q; want %q", tt.input, got, tt.expected)
246+
}
247+
})
248+
}
177249
}
178250

179251
func TestImportDataCmd_HandleCsvExecute(t *testing.T) {
@@ -680,7 +752,7 @@ func TestHandleCsv(t *testing.T) {
680752
assert.Equal(t, "test-project", projectId)
681753
assert.Equal(t, "test-instance", instanceId)
682754
assert.Equal(t, "test-db", dbName)
683-
assert.Equal(t, "test-table", tableName)
755+
assert.Equal(t, "testtable", tableName)
684756
assert.Equal(t, "gs://test-bucket/test_schema.json", schemaUri)
685757

686758
return &import_file.MockCsvSchema{}
@@ -689,7 +761,7 @@ func TestHandleCsv(t *testing.T) {
689761
assert.Equal(t, "test-project", projectId)
690762
assert.Equal(t, "test-instance", instanceId)
691763
assert.Equal(t, "test-db", dbName)
692-
assert.Equal(t, "test-table", tableName)
764+
assert.Equal(t, "testtable", tableName)
693765
assert.Equal(t, ",", csvFieldDelimiter)
694766
return &import_file.MockCsvData{}
695767
},

import_file/csv_schema.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,9 @@ func getCreateTableStmt(tableName string, colDef []ColumnDefinition, dialect str
151151

152152
var stmt string
153153
if dialect == constants.DIALECT_POSTGRESQL {
154-
stmt = fmt.Sprintf("CREATE TABLE %s (\n%s PRIMARY KEY (%s)\n)", quote(tableName), col, pk)
154+
stmt = fmt.Sprintf("CREATE TABLE %s (%s PRIMARY KEY (%s))", quote(tableName), col, pk)
155155
}
156-
stmt = fmt.Sprintf("CREATE TABLE %s (\n%s) PRIMARY KEY (%s)", quote(tableName), col, pk)
156+
stmt = fmt.Sprintf("CREATE TABLE %s (%s) PRIMARY KEY (%s)", quote(tableName), col, pk)
157157
logger.Log.Debug(fmt.Sprintf("create table cmd %s ==", stmt))
158158
return stmt
159159
}

import_file/csv_schema_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"testing"
8+
79
"github.com/GoogleCloudPlatform/spanner-migration-tool/file_reader"
810
"github.com/stretchr/testify/assert"
9-
"testing"
1011

1112
"cloud.google.com/go/spanner"
1213
"cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
@@ -186,7 +187,7 @@ func Test_getCreateTableStmt(t *testing.T) {
186187
{"col2", "STRING(MAX)", false, 2},
187188
},
188189
dialect: constants.DIALECT_GOOGLESQL,
189-
want: "CREATE TABLE `test_table` (\n`col1` INT64 NOT NULL ,`col2` STRING(MAX)) PRIMARY KEY (`col1`,`col2`)",
190+
want: "CREATE TABLE `test_table` (`col1` INT64 NOT NULL ,`col2` STRING(MAX)) PRIMARY KEY (`col1`,`col2`)",
190191
},
191192
{
192193
name: "Postgres Dialect",
@@ -196,7 +197,7 @@ func Test_getCreateTableStmt(t *testing.T) {
196197
{"col2", "STRING(MAX)", false, 2},
197198
},
198199
dialect: constants.DIALECT_POSTGRESQL,
199-
want: "CREATE TABLE `test_table` (\n`col1` INT64 NOT NULL ,`col2` STRING(MAX)) PRIMARY KEY (`col1`,`col2`)",
200+
want: "CREATE TABLE `test_table` (`col1` INT64 NOT NULL ,`col2` STRING(MAX)) PRIMARY KEY (`col1`,`col2`)",
200201
},
201202
}
202203

0 commit comments

Comments
 (0)