diff --git a/lib/debezium/converters/time.go b/lib/debezium/converters/time.go index 7bb4fe3cc..244ca419b 100644 --- a/lib/debezium/converters/time.go +++ b/lib/debezium/converters/time.go @@ -118,31 +118,25 @@ func (ZonedTimestamp) Convert(value any) (any, error) { return nil, fmt.Errorf("failed to parse %q, err: %w", valString, err) } -var SupportedTimeWithTimezoneFormats = []string{ - "15:04:05Z", // w/o fractional seconds - "15:04:05.000Z", // ms - "15:04:05.000000Z", // microseconds -} - type TimeWithTimezone struct{} -func (TimeWithTimezone) ToKindDetails() typing.KindDetails { - return typing.NewExtendedTimeDetails(typing.ETime, ext.TimeKindType, "") +func (t TimeWithTimezone) layout() string { + return "15:04:05.999999Z" +} + +func (t TimeWithTimezone) ToKindDetails() typing.KindDetails { + return typing.NewExtendedTimeDetails(typing.ETime, ext.TimeKindType, t.layout()) } -func (TimeWithTimezone) Convert(value any) (any, error) { +func (t TimeWithTimezone) Convert(value any) (any, error) { valString, isOk := value.(string) if !isOk { return nil, fmt.Errorf("expected string got '%v' with type %T", value, value) } - var err error - var ts time.Time - for _, supportedTimeFormat := range SupportedTimeWithTimezoneFormats { - ts, err = ext.ParseTimeExactMatch(supportedTimeFormat, valString) - if err == nil { - return ext.NewExtendedTime(ts, ext.TimeKindType, supportedTimeFormat), nil - } + ts, err := time.Parse(t.layout(), valString) + if err == nil { + return ext.NewExtendedTime(ts, ext.TimeKindType, t.layout()), nil } return nil, fmt.Errorf("failed to parse %q: %w", valString, err) diff --git a/lib/debezium/converters/time_test.go b/lib/debezium/converters/time_test.go index 67e68baa6..a25b3ffb9 100644 --- a/lib/debezium/converters/time_test.go +++ b/lib/debezium/converters/time_test.go @@ -165,29 +165,47 @@ func TestMicroTime_Converter(t *testing.T) { func TestConvertTimeWithTimezone(t *testing.T) { { // Invalid - ts, err := TimeWithTimezone{}.Convert("23:02") - assert.Nil(t, ts) - assert.ErrorContains(t, err, `failed to parse "23:02": parsing time`) + { + // Malformed + _, err := TimeWithTimezone{}.Convert("23:02") + assert.ErrorContains(t, err, `failed to parse "23:02": parsing time`) + } + { + // Non UTC + _, err := TimeWithTimezone{}.Convert("23:02:06.745116") + assert.ErrorContains(t, err, `failed to parse "23:02:06.745116"`) + } + { + // Providing timezone offset + _, err := TimeWithTimezone{}.Convert("23:02:06.745116Z-07:00") + assert.ErrorContains(t, err, `failed to parse "23:02:06.745116Z-07:00": parsing time "23:02:06.745116Z-07:00": extra text: "-07:00"`) + } } { - // What Debezium + Reader would produce + // What Debezium + Reader would produce (microsecond precision) val, err := TimeWithTimezone{}.Convert("23:02:06.745116Z") - ts, isOk := val.(*ext.ExtendedTime) - assert.True(t, isOk) assert.NoError(t, err) - expectedTs := ext.NewExtendedTime(time.Date(0, 1, 1, 23, 2, 6, 745116000, time.UTC), ext.TimeKindType, "15:04:05.000000Z") - assert.Equal(t, expectedTs, ts) + expectedTs := ext.NewExtendedTime(time.Date(0, 1, 1, 23, 2, 6, 745116000, time.UTC), ext.TimeKindType, "15:04:05.999999Z") + assert.Equal(t, expectedTs, val.(*ext.ExtendedTime)) + assert.Equal(t, "23:02:06.745116Z", val.(*ext.ExtendedTime).String("")) } { - // Non UTC - ts, err := TimeWithTimezone{}.Convert("23:02:06.745116") - assert.ErrorContains(t, err, `failed to parse "23:02:06.745116"`) - assert.Nil(t, ts) - - // Providing timezone offset - ts, err = TimeWithTimezone{}.Convert("23:02:06.745116Z-07:00") - assert.ErrorContains(t, err, `failed to parse "23:02:06.745116Z-07:00": parsing time "23:02:06.745116Z-07:00": extra text: "-07:00"`) - assert.Nil(t, ts) + // ms precision + val, err := TimeWithTimezone{}.Convert("23:02:06.745Z") + assert.NoError(t, err) + + expectedTs := ext.NewExtendedTime(time.Date(0, 1, 1, 23, 2, 6, 745000000, time.UTC), ext.TimeKindType, "15:04:05.999999Z") + assert.Equal(t, expectedTs, val.(*ext.ExtendedTime)) + assert.Equal(t, "23:02:06.745Z", val.(*ext.ExtendedTime).String("")) + } + { + // no fractional seconds + val, err := TimeWithTimezone{}.Convert("23:02:06Z") + assert.NoError(t, err) + + expectedTs := ext.NewExtendedTime(time.Date(0, 1, 1, 23, 2, 6, 0, time.UTC), ext.TimeKindType, "15:04:05.999999Z") + assert.Equal(t, expectedTs, val.(*ext.ExtendedTime)) + assert.Equal(t, "23:02:06Z", val.(*ext.ExtendedTime).String("")) } } diff --git a/lib/debezium/schema_test.go b/lib/debezium/schema_test.go index 5c80d4e41..ed1baabad 100644 --- a/lib/debezium/schema_test.go +++ b/lib/debezium/schema_test.go @@ -255,10 +255,12 @@ func TestField_ToKindDetails(t *testing.T) { } { { - // Time with timezone - kd, err := Field{DebeziumType: TimeWithTimezone}.ToKindDetails() - assert.NoError(t, err) - assert.Equal(t, typing.NewExtendedTimeDetails(typing.ETime, ext.TimeKindType, ""), kd) + // Time with time zone + for _, dbzType := range []SupportedDebeziumType{TimeWithTimezone} { + kd, err := Field{DebeziumType: dbzType}.ToKindDetails() + assert.NoError(t, err) + assert.Equal(t, typing.NewExtendedTimeDetails(typing.ETime, ext.TimeKindType, "15:04:05.999999Z"), kd, dbzType) + } } { // Time