diff --git a/.gitignore b/.gitignore index c8fad7b27..04e2c639d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ +.vscode/ parameters*.json parameters*.bat *.p8 @@ -11,6 +12,8 @@ wss-golang-agent.config wss-unified-agent.jar whitesource/ *.swp +cp.out +__debug_bin* # exclude vendor vendor diff --git a/converter.go b/converter.go index 347852395..741330cf0 100644 --- a/converter.go +++ b/converter.go @@ -28,6 +28,7 @@ import ( const format = "2006-01-02 15:04:05.999999999" const numberDefaultPrecision = 38 +const jsonFormatStr = "json" type timezoneType int @@ -239,11 +240,21 @@ func valueToString(v driver.Value, tsmode snowflakeType, params map[string]*stri logger.Debugf("TYPE: %v, %v", reflect.TypeOf(v), reflect.ValueOf(v)) if v == nil { if tsmode == objectType || tsmode == arrayType || tsmode == sliceType { - return bindingValue{nil, "json", nil}, nil + return bindingValue{nil, jsonFormatStr, nil}, nil } return bindingValue{nil, "", nil}, nil } v1 := reflect.Indirect(reflect.ValueOf(v)) + + if valuer, ok := v.(driver.Valuer); ok { // check for driver.Valuer satisfaction and honor that first + if value, err := valuer.Value(); err == nil && value != nil { + // if the output value is a valid string, return that + if strVal, ok := value.(string); ok { + return bindingValue{&strVal, "", nil}, nil + } + } + } + switch v1.Kind() { case reflect.Bool: s := strconv.FormatBool(v1.Bool()) @@ -257,7 +268,7 @@ func valueToString(v driver.Value, tsmode snowflakeType, params map[string]*stri case reflect.String: s := v1.String() if tsmode == objectType || tsmode == arrayType || tsmode == sliceType { - return bindingValue{&s, "json", nil}, nil + return bindingValue{&s, jsonFormatStr, nil}, nil } return bindingValue{&s, "", nil}, nil case reflect.Slice, reflect.Array: @@ -274,7 +285,7 @@ func valueToString(v driver.Value, tsmode snowflakeType, params map[string]*stri func arrayToString(v driver.Value, tsmode snowflakeType, params map[string]*string) (bindingValue, error) { v1 := reflect.Indirect(reflect.ValueOf(v)) if v1.Kind() == reflect.Slice && v1.IsNil() { - return bindingValue{nil, "json", nil}, nil + return bindingValue{nil, jsonFormatStr, nil}, nil } if bd, ok := v.([][]byte); ok && tsmode == binaryType { schema := bindingSchema{ @@ -289,14 +300,14 @@ func arrayToString(v driver.Value, tsmode snowflakeType, params map[string]*stri } if len(bd) == 0 { res := "[]" - return bindingValue{value: &res, format: "json", schema: &schema}, nil + return bindingValue{value: &res, format: jsonFormatStr, schema: &schema}, nil } s := "" for _, b := range bd { s += "\"" + hex.EncodeToString(b) + "\"," } s = "[" + s[:len(s)-1] + "]" - return bindingValue{&s, "json", &schema}, nil + return bindingValue{&s, jsonFormatStr, &schema}, nil } else if times, ok := v.([]time.Time); ok { typ := driverTypeToSnowflake[tsmode] sfFormat, err := dateTimeInputFormatByType(typ, params) @@ -313,7 +324,7 @@ func arrayToString(v driver.Value, tsmode snowflakeType, params map[string]*stri } res, err := json.Marshal(v) if err != nil { - return bindingValue{nil, "json", &bindingSchema{ + return bindingValue{nil, jsonFormatStr, &bindingSchema{ Typ: "array", Nullable: true, Fields: []fieldMetadata{ @@ -325,7 +336,7 @@ func arrayToString(v driver.Value, tsmode snowflakeType, params map[string]*stri }}, err } resString := string(res) - return bindingValue{&resString, "json", nil}, nil + return bindingValue{&resString, jsonFormatStr, nil}, nil } else if isArrayOfStructs(v) { stringEntries := make([]string, v1.Len()) sowcForSingleElement, err := buildSowcFromType(params, reflect.TypeOf(v).Elem()) @@ -337,7 +348,7 @@ func arrayToString(v driver.Value, tsmode snowflakeType, params map[string]*stri if sow, ok := potentialSow.Interface().(StructuredObjectWriter); ok { bv, err := structValueToString(sow, tsmode, params) if err != nil { - return bindingValue{nil, "json", nil}, err + return bindingValue{nil, jsonFormatStr, nil}, err } stringEntries[i] = *bv.value } @@ -354,14 +365,14 @@ func arrayToString(v driver.Value, tsmode snowflakeType, params map[string]*stri }, }, } - return bindingValue{&value, "json", arraySchema}, nil + return bindingValue{&value, jsonFormatStr, arraySchema}, nil } else if reflect.ValueOf(v).Len() == 0 { value := "[]" - return bindingValue{&value, "json", nil}, nil + return bindingValue{&value, jsonFormatStr, nil}, nil } else if barr, ok := v.([]byte); ok { if tsmode == binaryType { res := hex.EncodeToString(barr) - return bindingValue{&res, "json", nil}, nil + return bindingValue{&res, jsonFormatStr, nil}, nil } schemaForBytes := bindingSchema{ Typ: "array", @@ -375,23 +386,27 @@ func arrayToString(v driver.Value, tsmode snowflakeType, params map[string]*stri } if len(barr) == 0 { res := "[]" - return bindingValue{&res, "json", &schemaForBytes}, nil + return bindingValue{&res, jsonFormatStr, &schemaForBytes}, nil } res := "[" for _, b := range barr { res += fmt.Sprint(b) + "," } res = res[0:len(res)-1] + "]" - return bindingValue{&res, "json", &schemaForBytes}, nil + return bindingValue{&res, jsonFormatStr, &schemaForBytes}, nil + } else if stringer, ok := v1.Interface().(fmt.Stringer); ok && v1.Kind() == reflect.Array && v1.Type().Elem().Kind() == reflect.Uint8 && v1.Len() == 16 { + // special case for UUIDs (snowflake type and other implementers) check for stringer method and it's a len 16 byte array. Guarantees it's String() and returns string + value := stringer.String() + return bindingValue{&value, "", nil}, nil } else if isSliceOfSlices(v) { return bindingValue{}, errors.New("array of arrays is not supported") } res, err := json.Marshal(v) if err != nil { - return bindingValue{nil, "json", nil}, err + return bindingValue{nil, jsonFormatStr, nil}, err } resString := string(res) - return bindingValue{&resString, "json", nil}, nil + return bindingValue{&resString, jsonFormatStr, nil}, nil } func mapToString(v driver.Value, tsmode snowflakeType, params map[string]*string) (bindingValue, error) { @@ -549,7 +564,7 @@ func mapToString(v driver.Value, tsmode snowflakeType, params map[string]*string Typ: "MAP", Fields: []fieldMetadata{keyMetadata, *valueMetadata}, } - return bindingValue{&jsonString, "json", &schema}, nil + return bindingValue{&jsonString, jsonFormatStr, &schema}, nil } else { jsonBytes, err = json.Marshal(v) if err != nil { @@ -569,7 +584,7 @@ func mapToString(v driver.Value, tsmode snowflakeType, params map[string]*string Typ: "MAP", Fields: []fieldMetadata{keyMetadata, valueMetadata}, } - return bindingValue{&jsonString, "json", &schema}, nil + return bindingValue{&jsonString, jsonFormatStr, &schema}, nil } func toNullableInt64(val any) (int64, bool) { @@ -771,7 +786,7 @@ func structValueToString(v driver.Value, tsmode snowflakeType, params map[string case sql.NullString: fmt := "" if tsmode == objectType || tsmode == arrayType || tsmode == sliceType { - fmt = "json" + fmt = jsonFormatStr } if !typedVal.Valid { return bindingValue{nil, fmt, nil}, nil @@ -795,7 +810,7 @@ func structValueToString(v driver.Value, tsmode snowflakeType, params map[string Nullable: true, Fields: sowc.toFields(), } - return bindingValue{&jsonString, "json", &schema}, nil + return bindingValue{&jsonString, jsonFormatStr, &schema}, nil } else if typ, ok := v.(reflect.Type); ok && tsmode == nilArrayType { metadata, err := goTypeToFieldMetadata(typ, tsmode, params) if err != nil { @@ -808,7 +823,7 @@ func structValueToString(v driver.Value, tsmode snowflakeType, params map[string metadata, }, } - return bindingValue{nil, "json", &schema}, nil + return bindingValue{nil, jsonFormatStr, &schema}, nil } else if types, ok := v.(NilMapTypes); ok && tsmode == nilMapType { keyMetadata, err := goTypeToFieldMetadata(types.Key, tsmode, params) if err != nil { @@ -823,7 +838,7 @@ func structValueToString(v driver.Value, tsmode snowflakeType, params map[string Nullable: true, Fields: []fieldMetadata{keyMetadata, valueMetadata}, } - return bindingValue{nil, "json", &schema}, nil + return bindingValue{nil, jsonFormatStr, &schema}, nil } else if typ, ok := v.(reflect.Type); ok && tsmode == nilObjectType { metadata, err := goTypeToFieldMetadata(typ, tsmode, params) if err != nil { @@ -834,7 +849,7 @@ func structValueToString(v driver.Value, tsmode snowflakeType, params map[string Nullable: true, Fields: metadata.Fields, } - return bindingValue{nil, "json", &schema}, nil + return bindingValue{nil, jsonFormatStr, &schema}, nil } return bindingValue{}, fmt.Errorf("unknown binding for type %T and mode %v", v, tsmode) } diff --git a/converter_test.go b/converter_test.go index 102660d8d..c8239f631 100644 --- a/converter_test.go +++ b/converter_test.go @@ -214,6 +214,16 @@ func (o *testValueToStringStructuredObject) Write(sowc StructuredObjectWriterCon return nil } +type testSqlUuid UUID + +func (uuid testSqlUuid) Value() (driver.Value, error) { + return uuid.String(), nil +} + +func (u testSqlUuid) String() string { + return fmt.Sprintf("%x-%x-%x-%x-%x", u[0:4], u[4:6], u[6:8], u[8:10], u[10:]) +} + func TestValueToString(t *testing.T) { v := cmplx.Sqrt(-5 + 12i) // should never happen as Go sql package must have already validated. _, err := valueToString(v, nullType, nil) @@ -269,22 +279,46 @@ func TestValueToString(t *testing.T) { assertNilE(t, bv.schema) assertEqualE(t, *bv.value, expectedString) + t.Run("SQL Time", func(t *testing.T) { + bv, err := valueToString(sql.NullTime{Time: localTime, Valid: true}, timestampLtzType, nil) + assertNilF(t, err) + assertEmptyStringE(t, bv.format) + assertNilE(t, bv.schema) + assertEqualE(t, *bv.value, expectedUnixTime) + }) + t.Run("arrays", func(t *testing.T) { bv, err := valueToString([2]int{1, 2}, objectType, nil) assertNilF(t, err) - assertEqualE(t, bv.format, "json") + assertEqualE(t, bv.format, jsonFormatStr) assertEqualE(t, *bv.value, "[1,2]") }) t.Run("slices", func(t *testing.T) { bv, err := valueToString([]int{1, 2}, objectType, nil) assertNilF(t, err) - assertEqualE(t, bv.format, "json") + assertEqualE(t, bv.format, jsonFormatStr) assertEqualE(t, *bv.value, "[1,2]") }) + t.Run("UUID - should return string", func(t *testing.T) { + u := NewUUID() + bv, err := valueToString(u, textType, nil) + assertNilF(t, err) + assertEmptyStringE(t, bv.format) + assertEqualE(t, *bv.value, u.String()) + }) + + t.Run("database/sql/driver - Valuer interface", func(t *testing.T) { + u := testSqlUuid(NewUUID()) + bv, err := valueToString(u, textType, nil) + assertNilF(t, err) + assertEmptyStringE(t, bv.format) + assertEqualE(t, *bv.value, u.String()) + }) + bv, err = valueToString(&testValueToStringStructuredObject{s: "some string", i: 123, date: time.Date(2024, time.May, 24, 0, 0, 0, 0, time.UTC)}, timestampLtzType, params) assertNilF(t, err) - assertEqualE(t, bv.format, "json") + assertEqualE(t, bv.format, jsonFormatStr) assertDeepEqualE(t, *bv.schema, bindingSchema{ Typ: "object", Nullable: true,