Repository: milvus-io/milvus-sdk-go
Pull request #824 (2024-09-23 02:13:50)

enhance: [2.4] Support embedded struct field as Row input (#823)
Cherry-pick from master
pr: #823 
Related to #818

sre-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/milvus-io/milvus-sdk-go/pull/824#" title="Author self-approved">congqixia</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=milvus-io%2Fmilvus-sdk-go).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/milvus-io/milvus-sdk-go/blob/v2.4.x/OWNERS)~~ [congqixia]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
congqixia: /kind enhancement

Patch:
diff --git a/entity/rows.go b/entity/rows.go
index dc30af7..7577523 100644
--- a/entity/rows.go
+++ b/entity/rows.go
@@ -32,7 +32,7 @@ const (
 	// MilvusTagSep struct tag const for attribute separator
 	MilvusTagSep = `;`
 
-	//MilvusTagName struct tag const for field name
+	// MilvusTagName struct tag const for field name
 	MilvusTagName = `NAME`
 
 	// VectorDimTag struct tag const for vector dimension
@@ -171,7 +171,7 @@ func ParseSchemaAny(r interface{}) (*Schema, error) {
 			switch elemType.Kind() {
 			case reflect.Uint8:
 				field.DataType = FieldTypeBinaryVector
-				//TODO maybe override by tag settings, when dim is not multiplier of 8
+				// TODO maybe override by tag settings, when dim is not multiplier of 8
 				field.TypeParams = map[string]string{
 					TypeParamDim: strconv.FormatInt(int64(arrayLen*8), 10),
 				}
@@ -526,6 +526,22 @@ func reflectValueCandi(v reflect.Value) (map[string]fieldCandi, error) {
 	case reflect.Struct:
 		for i := 0; i < v.NumField(); i++ {
 			ft := v.Type().Field(i)
+			// embedded struct
+			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 {
+					_, 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
+			}
+
 			name := ft.Name
 			tag, ok := ft.Tag.Lookup(MilvusTag)
 


Test patch:
diff --git a/entity/rows_test.go b/entity/rows_test.go
index 19d182d..81c633d 100644
--- a/entity/rows_test.go
+++ b/entity/rows_test.go
@@ -57,7 +57,6 @@ type SliceBadDimStruct2 struct {
 }
 
 func TestParseSchema(t *testing.T) {
-
 	t.Run("invalid cases", func(t *testing.T) {
 		// anonymous struct with default collection name ("") will cause error
 		anonymusStruct := struct {
@@ -108,11 +107,9 @@ func TestParseSchema(t *testing.T) {
 		sch, err = ParseSchema(&SliceBadDimStruct2{})
 		assert.Nil(t, sch)
 		assert.NotNil(t, err)
-
 	})
 
 	t.Run("valid cases", func(t *testing.T) {
-
 		sch, err := ParseSchema(RowBase{})
 		assert.Nil(t, err)
 		assert.Equal(t, "RowBase", sch.CollectionName)
@@ -268,7 +265,6 @@ type RowsSuite struct {
 
 func (s *RowsSuite) TestRowsToColumns() {
 	s.Run("valid_cases", func() {
-
 		columns, err := RowsToColumns([]Row{&ValidStruct{}})
 		s.Nil(err)
 		s.Equal(10, len(columns))
@@ -358,6 +354,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
@@ -381,6 +381,42 @@ 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_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,
+		},
 	}
 
 	for _, c := range cases {
diff --git a/test/common/response_check.go b/test/common/response_check.go
index 1a5840d..2d0b853 100644
--- a/test/common/response_check.go
+++ b/test/common/response_check.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"io"
 	"log"
+	"reflect"
 	"strings"
 	"testing"
 
@@ -155,9 +156,15 @@ func EqualColumn(t *testing.T, columnA entity.Column, columnB entity.Column) {
 	case entity.FieldTypeVarChar:
 		require.ElementsMatch(t, columnA.(*entity.ColumnVarChar).Data(), columnB.(*entity.ColumnVarChar).Data())
 	case entity.FieldTypeJSON:
-		log.Printf("columnA: %s", columnA.(*entity.ColumnJSONBytes).Data())
-		log.Printf("columnB: %s", columnB.(*entity.ColumnJSONBytes).Data())
-		require.ElementsMatch(t, columnA.(*entity.ColumnJSONBytes).Data(), columnB.(*entity.ColumnJSONBytes).Data())
+		log.Printf("columnA: %s", columnA.FieldData())
+		log.Printf("columnB: %s", columnB.FieldData())
+		require.Equal(t, reflect.TypeOf(columnA), reflect.TypeOf(columnB))
+		switch columnA.(type) {
+		case *entity.ColumnDynamic:
+			require.ElementsMatch(t, columnA.(*entity.ColumnDynamic).Data(), columnB.(*entity.ColumnDynamic).Data())
+		case *entity.ColumnJSONBytes:
+			require.ElementsMatch(t, columnA.(*entity.ColumnJSONBytes).Data(), columnB.(*entity.ColumnJSONBytes).Data())
+		}
 	case entity.FieldTypeFloatVector:
 		require.ElementsMatch(t, columnA.(*entity.ColumnFloatVector).Data(), columnB.(*entity.ColumnFloatVector).Data())
 	case entity.FieldTypeBinaryVector:
diff --git a/test/testcases/query_test.go b/test/testcases/query_test.go
index 2833fad..6e089ee 100644
--- a/test/testcases/query_test.go
+++ b/test/testcases/query_test.go
@@ -984,12 +984,16 @@ func TestQueryJsonDynamicExpr(t *testing.T) {
 	// verify output fields and count, dynamicNumber value
 	common.CheckOutputFields(t, queryRes, []string{common.DefaultIntFieldName, common.DefaultJSONFieldName, common.DefaultDynamicNumberField})
 	require.Equal(t, 10, queryRes.GetColumn(common.DefaultJSONFieldName).Len())
+
+	// verify only output dynamic part key: common.DefaultDynamicNumberField
 	dynamicNumColumn := queryRes.GetColumn(common.DefaultDynamicNumberField)
-	var numberData []int64
-	for i := 0; i < dynamicNumColumn.Len(); i++ {
-		line, _ := dynamicNumColumn.GetAsInt64(i)
-		numberData = append(numberData, line)
+	numberValues := make([]int32, 0, 10)
+	for i := 0; i < 10; i++ {
+		numberValues = append(numberValues, int32(i))
 	}
+	_expColumn := common.MergeColumnsToDynamic(10, common.GenDynamicFieldData(0, 10)[:1], common.DefaultDynamicFieldName)
+	expColumn := entity.NewColumnDynamic(_expColumn, common.DefaultDynamicNumberField)
+	common.EqualColumn(t, dynamicNumColumn, expColumn)
 }
 
 // test query and output both json and dynamic field