From 7c11c159963b8fbdc0fc136fd954a0c642c4dfcc Mon Sep 17 00:00:00 2001 From: monkey92t Date: Thu, 2 Feb 2023 00:27:45 +0800 Subject: [PATCH 1/4] feat(scan): scan time.Time uses `UnmarshalText(RFC3339)` interface decoding by default Signed-off-by: monkey92t --- commands_test.go | 19 +++++++++++++++++++ internal/hscan/hscan.go | 17 ++++++++++++++++- internal/hscan/hscan_test.go | 12 ++++++++++++ internal/proto/writer.go | 13 +++++++------ 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/commands_test.go b/commands_test.go index 9bc277d45..dd6c9027e 100644 --- a/commands_test.go +++ b/commands_test.go @@ -1841,6 +1841,25 @@ var _ = Describe("Commands", func() { Key2: 123, Time: TimeValue{Time: time.Time{}}, })) + + type data2 struct { + Key1 string `redis:"key1"` + Key2 int `redis:"key2"` + Time time.Time `redis:"time"` + } + err = client.HSet(ctx, "hash", &data2{ + Key1: "hello2", + Key2: 200, + Time: now, + }).Err() + Expect(err).NotTo(HaveOccurred()) + + var d2 data2 + err = client.HMGet(ctx, "hash", "key1", "key2", "time").Scan(&d2) + Expect(err).NotTo(HaveOccurred()) + Expect(d2.Key1).To(Equal("hello2")) + Expect(d2.Key2).To(Equal(200)) + Expect(d2.Time.Unix()).To(Equal(now.Unix())) }) It("should HIncrBy", func() { diff --git a/internal/hscan/hscan.go b/internal/hscan/hscan.go index 203ec4aa8..fce7ff32c 100644 --- a/internal/hscan/hscan.go +++ b/internal/hscan/hscan.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "strconv" + "time" ) // decoderFunc represents decoding functions for default built-in types. @@ -42,7 +43,7 @@ var ( reflect.Ptr: decodeUnsupported, reflect.Slice: decodeSlice, reflect.String: decodeString, - reflect.Struct: decodeUnsupported, + reflect.Struct: decodeStruct, reflect.UnsafePointer: decodeUnsupported, } @@ -202,6 +203,20 @@ func decodeSlice(f reflect.Value, s string) error { return nil } +func decodeStruct(f reflect.Value, s string) error { + // handle time.Time here. + // time.Time CanSet() == false + if f.CanAddr() { + p := f.Addr() + if p.CanInterface() { + if v, ok := p.Interface().(*time.Time); ok { + return v.UnmarshalText([]byte(s)) + } + } + } + return decodeUnsupported(f, s) +} + func decodeUnsupported(v reflect.Value, s string) error { return fmt.Errorf("redis.Scan(unsupported %s)", v.Type()) } diff --git a/internal/hscan/hscan_test.go b/internal/hscan/hscan_test.go index 9d6df01b0..728ffad0a 100644 --- a/internal/hscan/hscan_test.go +++ b/internal/hscan/hscan_test.go @@ -200,4 +200,16 @@ var _ = Describe("Scan", func() { Expect(td.Time.UnixNano()).To(Equal(now.UnixNano())) Expect(td.Time.Format(time.RFC3339Nano)).To(Equal(now.Format(time.RFC3339Nano))) }) + + It("should time.Time RFC3339Nano", func() { + type TimeTime struct { + Time time.Time `redis:"time"` + } + + now := time.Now() + + var tt TimeTime + Expect(Scan(&tt, i{"time"}, i{now.Format(time.RFC3339Nano)})).NotTo(HaveOccurred()) + Expect(now.Unix()).To(Equal(tt.Time.Unix())) + }) }) diff --git a/internal/proto/writer.go b/internal/proto/writer.go index 8eaada196..1f2378301 100644 --- a/internal/proto/writer.go +++ b/internal/proto/writer.go @@ -101,15 +101,16 @@ func (w *Writer) WriteArg(v interface{}) error { return w.bytes(w.numBuf) case time.Duration: return w.int(v.Nanoseconds()) - case encoding.BinaryMarshaler: - b, err := v.MarshalBinary() - if err != nil { - return err - } - return w.bytes(b) case net.IP: return w.bytes(v) default: + if m, ok := v.(encoding.BinaryMarshaler); ok { + b, err := m.MarshalBinary() + if err != nil { + return err + } + return w.bytes(b) + } return fmt.Errorf( "redis: can't marshal %T (implement encoding.BinaryMarshaler)", v) } From c6c80fa2051817f0abf9eaf173eb3b331818a696 Mon Sep 17 00:00:00 2001 From: monkey92t Date: Thu, 2 Feb 2023 21:32:22 +0800 Subject: [PATCH 2/4] perf: revert encoding.BinaryMarshaler Signed-off-by: monkey92t --- internal/proto/writer.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/internal/proto/writer.go b/internal/proto/writer.go index 1f2378301..8eaada196 100644 --- a/internal/proto/writer.go +++ b/internal/proto/writer.go @@ -101,16 +101,15 @@ func (w *Writer) WriteArg(v interface{}) error { return w.bytes(w.numBuf) case time.Duration: return w.int(v.Nanoseconds()) + case encoding.BinaryMarshaler: + b, err := v.MarshalBinary() + if err != nil { + return err + } + return w.bytes(b) case net.IP: return w.bytes(v) default: - if m, ok := v.(encoding.BinaryMarshaler); ok { - b, err := m.MarshalBinary() - if err != nil { - return err - } - return w.bytes(b) - } return fmt.Errorf( "redis: can't marshal %T (implement encoding.BinaryMarshaler)", v) } From a1d066873ee43b34cc332d3ff838797f8aa504ba Mon Sep 17 00:00:00 2001 From: monkey92t Date: Fri, 3 Feb 2023 00:13:57 +0800 Subject: [PATCH 3/4] perf(scan): Scan asserts the encoding.TextUnmarshaler interface Signed-off-by: monkey92t --- internal/hscan/hscan.go | 17 +---------------- internal/hscan/structmap.go | 8 +++++++- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/internal/hscan/hscan.go b/internal/hscan/hscan.go index fce7ff32c..203ec4aa8 100644 --- a/internal/hscan/hscan.go +++ b/internal/hscan/hscan.go @@ -5,7 +5,6 @@ import ( "fmt" "reflect" "strconv" - "time" ) // decoderFunc represents decoding functions for default built-in types. @@ -43,7 +42,7 @@ var ( reflect.Ptr: decodeUnsupported, reflect.Slice: decodeSlice, reflect.String: decodeString, - reflect.Struct: decodeStruct, + reflect.Struct: decodeUnsupported, reflect.UnsafePointer: decodeUnsupported, } @@ -203,20 +202,6 @@ func decodeSlice(f reflect.Value, s string) error { return nil } -func decodeStruct(f reflect.Value, s string) error { - // handle time.Time here. - // time.Time CanSet() == false - if f.CanAddr() { - p := f.Addr() - if p.CanInterface() { - if v, ok := p.Interface().(*time.Time); ok { - return v.UnmarshalText([]byte(s)) - } - } - } - return decodeUnsupported(f, s) -} - func decodeUnsupported(v reflect.Value, s string) error { return fmt.Errorf("redis.Scan(unsupported %s)", v.Type()) } diff --git a/internal/hscan/structmap.go b/internal/hscan/structmap.go index 9befd9873..70cce9c39 100644 --- a/internal/hscan/structmap.go +++ b/internal/hscan/structmap.go @@ -1,10 +1,13 @@ package hscan import ( + "encoding" "fmt" "reflect" "strings" "sync" + + "github.com/redis/go-redis/v9/internal/util" ) // structMap contains the map of struct fields for target structs @@ -97,7 +100,10 @@ func (s StructValue) Scan(key string, value string) error { } if isPtr && v.Type().NumMethod() > 0 && v.CanInterface() { - if scan, ok := v.Interface().(Scanner); ok { + switch scan := v.Interface().(type) { + case encoding.TextUnmarshaler: + return scan.UnmarshalText(util.StringToBytes(value)) + case Scanner: return scan.ScanRedis(value) } } From a1f7e98412f833abcecb83406f11d360b2c9aec1 Mon Sep 17 00:00:00 2001 From: monkey92t Date: Sun, 5 Feb 2023 23:09:32 +0800 Subject: [PATCH 4/4] feat: swap Scanner and encoding.TextUnmarshaler Signed-off-by: monkey92t --- internal/hscan/structmap.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/hscan/structmap.go b/internal/hscan/structmap.go index 70cce9c39..8d6028ef9 100644 --- a/internal/hscan/structmap.go +++ b/internal/hscan/structmap.go @@ -101,10 +101,10 @@ func (s StructValue) Scan(key string, value string) error { if isPtr && v.Type().NumMethod() > 0 && v.CanInterface() { switch scan := v.Interface().(type) { - case encoding.TextUnmarshaler: - return scan.UnmarshalText(util.StringToBytes(value)) case Scanner: return scan.ScanRedis(value) + case encoding.TextUnmarshaler: + return scan.UnmarshalText(util.StringToBytes(value)) } }