diff --git a/client/row/data.go b/client/row/data.go index eff621b51c..e774a55b86 100644 --- a/client/row/data.go +++ b/client/row/data.go @@ -276,60 +276,91 @@ type fieldCandi struct { } func reflectValueCandi(v reflect.Value) (map[string]fieldCandi, error) { - if v.Kind() == reflect.Ptr { + // unref **/***/... struct{} + for v.Kind() == reflect.Ptr { v = v.Elem() } - result := make(map[string]fieldCandi) switch v.Kind() { case reflect.Map: // map[string]any - iter := v.MapRange() - for iter.Next() { - key := iter.Key().String() - result[key] = fieldCandi{ - name: key, - v: iter.Value(), - } - } - return result, nil + return getMapReflectCandidates(v), nil case reflect.Struct: - for i := 0; i < v.NumField(); i++ { - ft := v.Type().Field(i) - name := ft.Name - tag, ok := ft.Tag.Lookup(MilvusTag) - - settings := make(map[string]string) - if ok { - if tag == MilvusSkipTagValue { - continue - } - settings = ParseTagSetting(tag, MilvusTagSep) - fn, has := settings[MilvusTagName] - if has { - // overwrite column to tag name - name = fn - } - } - _, ok = result[name] - // duplicated - if ok { - return nil, fmt.Errorf("column has duplicated name: %s when parsing field: %s", name, ft.Name) - } - - v := v.Field(i) - if v.Kind() == reflect.Array { - v = v.Slice(0, v.Len()) - } - - result[name] = fieldCandi{ - name: name, - v: v, - options: settings, - } - } - - return result, nil + return getStructReflectCandidates(v) default: return nil, fmt.Errorf("unsupport row type: %s", v.Kind().String()) } } + +// getMapReflectCandidates converts input map into fieldCandidate struct. +// if value is struct/map etc, it will be treated as json data type directly(if schema say so). +func getMapReflectCandidates(v reflect.Value) map[string]fieldCandi { + result := make(map[string]fieldCandi) + iter := v.MapRange() + for iter.Next() { + key := iter.Key().String() + result[key] = fieldCandi{ + name: key, + v: iter.Value(), + } + } + return result +} + +// getStructReflectCandidates parses struct fields into fieldCandidates. +// embedded struct will be flatten as field as well. +func getStructReflectCandidates(v reflect.Value) (map[string]fieldCandi, error) { + result := make(map[string]fieldCandi) + for i := 0; i < v.NumField(); i++ { + ft := v.Type().Field(i) + name := ft.Name + + // embedded struct, flatten all fields + if ft.Anonymous && ft.Type.Kind() == reflect.Struct { + embedCandidate, err := reflectValueCandi(v.Field(i)) + if err != nil { + return nil, err + } + for key, candi := range embedCandidate { + // check duplicated field name in different structs + _, ok := result[key] + if ok { + return nil, fmt.Errorf("column has duplicated name: %s when parsing field: %s", key, ft.Name) + } + result[key] = candi + } + continue + } + + tag, ok := ft.Tag.Lookup(MilvusTag) + settings := make(map[string]string) + if ok { + if tag == MilvusSkipTagValue { + continue + } + settings = ParseTagSetting(tag, MilvusTagSep) + fn, has := settings[MilvusTagName] + if has { + // overwrite column to tag name + name = fn + } + } + _, ok = result[name] + // duplicated + if ok { + return nil, fmt.Errorf("column has duplicated name: %s when parsing field: %s", name, ft.Name) + } + + v := v.Field(i) + if v.Kind() == reflect.Array { + v = v.Slice(0, v.Len()) + } + + result[name] = fieldCandi{ + name: name, + v: v, + options: settings, + } + } + + return result, nil +} diff --git a/client/row/data_test.go b/client/row/data_test.go index 9e8b7fb216..87e73946d8 100644 --- a/client/row/data_test.go +++ b/client/row/data_test.go @@ -1,6 +1,7 @@ package row import ( + "fmt" "reflect" "testing" @@ -126,6 +127,10 @@ func (s *RowsSuite) TestDynamicSchema() { } func (s *RowsSuite) TestReflectValueCandi() { + type DynamicRows struct { + Float float32 `json:"float" milvus:"name:float"` + } + cases := []struct { tag string v reflect.Value @@ -149,6 +154,65 @@ func (s *RowsSuite) TestReflectValueCandi() { }, expectErr: false, }, + { + tag: "StructRow", + v: reflect.ValueOf(struct { + A string + B int64 + }{A: "abc", B: 16}), + expect: map[string]fieldCandi{ + "A": { + name: "A", + v: reflect.ValueOf("abc"), + }, + "B": { + name: "B", + v: reflect.ValueOf(int64(16)), + }, + }, + expectErr: false, + }, + { + tag: "StructRow_DuplicateName", + v: reflect.ValueOf(struct { + A string `milvus:"name:a"` + B int64 `milvus:"name:a"` + }{A: "abc", B: 16}), + expectErr: true, + }, + { + tag: "StructRow_EmbedStruct", + v: reflect.ValueOf(struct { + A string `milvus:"name:a"` + DynamicRows + }{A: "emb", DynamicRows: DynamicRows{Float: 0.1}}), + expect: map[string]fieldCandi{ + "a": { + name: "a", + v: reflect.ValueOf("emb"), + }, + "float": { + name: "float", + v: reflect.ValueOf(float32(0.1)), + }, + }, + expectErr: false, + }, + { + tag: "StructRow_EmbedDuplicateName", + v: reflect.ValueOf(struct { + Int64 int64 `json:"int64" milvus:"name:int64"` + Float float32 `json:"float" milvus:"name:float"` + FloatVec []float32 `json:"floatVec" milvus:"name:floatVec"` + DynamicRows + }{}), + expectErr: true, + }, + { + tag: "Unsupported_primitive", + v: reflect.ValueOf(int64(1)), + expectErr: true, + }, } for _, c := range cases { @@ -162,7 +226,7 @@ func (s *RowsSuite) TestReflectValueCandi() { s.Equal(len(c.expect), len(r)) for k, v := range c.expect { rv, has := r[k] - s.Require().True(has) + s.Require().True(has, fmt.Sprintf("candidate with key(%s) must provided", k)) s.Equal(v.name, rv.name) } })