mirror of
https://github.com/milvus-io/milvus.git
synced 2026-07-21 10:15:43 +00:00
enhance: [GoSDK] support pointer types for nullable columns in row-based API (#47712)
Related to #47711 Enable Go pointer struct fields (*string, *int32, etc.) to represent nullable columns in the row-based data processing path. A nil pointer maps to a NULL value in Milvus; a non-nil pointer dereferences to the actual value. Changes: - ParseSchema: pointer fields auto-infer Nullable=true (PK excluded) - AnyToColumns: use reflect.Value.IsNil() to call AppendNull for nil pointers, Elem().Interface() for non-nil — bypasses Go's typed-nil problem where (*T)(nil) as interface{} is != nil - SetField: wrap/unwrap pointer values during field assignment - fillData/fillPKEntry: check IsNull() on columns and set nil pointers for null values, wrap non-null values in reflect.New pointers - Dynamic field path: same nil/deref logic for pointer fields going into the JSON dynamic column Signed-off-by: Congqi Xia <congqi.xia@zilliz.com>
This commit is contained in:
@@ -148,7 +148,14 @@ func (sr *ResultSet) fillPKEntry(receiver any) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row.Field(candi).Set(reflect.ValueOf(val))
|
||||
field := row.Field(candi)
|
||||
if field.Kind() == reflect.Ptr {
|
||||
ptr := reflect.New(field.Type().Elem())
|
||||
ptr.Elem().Set(reflect.ValueOf(val))
|
||||
field.Set(ptr)
|
||||
} else {
|
||||
field.Set(reflect.ValueOf(val))
|
||||
}
|
||||
}
|
||||
rr.Set(rv)
|
||||
default:
|
||||
@@ -227,12 +234,33 @@ func (ds DataSet) fillData(data reflect.Value, dataType reflect.Type, idx int) e
|
||||
// `strict` mode could be added in the future to return error if any column missing
|
||||
continue
|
||||
}
|
||||
val, err := ds[i].Get(idx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
field := data.Field(fidx)
|
||||
fieldType := dataType.Field(fidx).Type
|
||||
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
isNull, err := ds[i].IsNull(idx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isNull {
|
||||
field.Set(reflect.Zero(fieldType))
|
||||
continue
|
||||
}
|
||||
val, err := ds[i].Get(idx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ptr := reflect.New(fieldType.Elem())
|
||||
ptr.Elem().Set(reflect.ValueOf(val))
|
||||
field.Set(ptr)
|
||||
} else {
|
||||
val, err := ds[i].Get(idx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.Set(reflect.ValueOf(val))
|
||||
}
|
||||
// TODO check datatype, return error here instead of reflect panicking & recover
|
||||
data.Field(fidx).Set(reflect.ValueOf(val))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -140,6 +140,90 @@ func (s *ResultSetSuite) TestSearchResultUnmarshal() {
|
||||
s.Error(err)
|
||||
}
|
||||
|
||||
func (s *ResultSetSuite) TestResultsetUnmarshalNullablePointer() {
|
||||
type NullableData struct {
|
||||
ID int64 `milvus:"name:id"`
|
||||
Name *string `milvus:"name:name"`
|
||||
Age *int32 `milvus:"name:age"`
|
||||
}
|
||||
|
||||
// Create a nullable string column with mixed null/non-null values
|
||||
nameCol := column.NewColumnVarChar("name", nil)
|
||||
nameCol.SetNullable(true)
|
||||
_ = nameCol.AppendValue("alice")
|
||||
_ = nameCol.AppendNull()
|
||||
_ = nameCol.AppendValue("charlie")
|
||||
|
||||
// Create a nullable int32 column
|
||||
ageCol := column.NewColumnInt32("age", nil)
|
||||
ageCol.SetNullable(true)
|
||||
_ = ageCol.AppendValue(int32(30))
|
||||
_ = ageCol.AppendValue(int32(25))
|
||||
_ = ageCol.AppendNull()
|
||||
|
||||
rs := DataSet([]column.Column{
|
||||
column.NewColumnInt64("id", []int64{1, 2, 3}),
|
||||
nameCol,
|
||||
ageCol,
|
||||
})
|
||||
|
||||
var receiver []*NullableData
|
||||
err := rs.Unmarshal(&receiver)
|
||||
s.NoError(err)
|
||||
s.Require().Len(receiver, 3)
|
||||
|
||||
// Row 0: Name="alice", Age=30
|
||||
s.Require().NotNil(receiver[0].Name)
|
||||
s.Equal("alice", *receiver[0].Name)
|
||||
s.Require().NotNil(receiver[0].Age)
|
||||
s.Equal(int32(30), *receiver[0].Age)
|
||||
|
||||
// Row 1: Name=nil, Age=25
|
||||
s.Nil(receiver[1].Name)
|
||||
s.Require().NotNil(receiver[1].Age)
|
||||
s.Equal(int32(25), *receiver[1].Age)
|
||||
|
||||
// Row 2: Name="charlie", Age=nil
|
||||
s.Require().NotNil(receiver[2].Name)
|
||||
s.Equal("charlie", *receiver[2].Name)
|
||||
s.Nil(receiver[2].Age)
|
||||
}
|
||||
|
||||
func (s *ResultSetSuite) TestSearchResultUnmarshalPointerPK() {
|
||||
type PtrPKData struct {
|
||||
A *int64 `milvus:"name:id"`
|
||||
V []float32 `milvus:"name:vector"`
|
||||
}
|
||||
|
||||
idData := []int64{1, 2, 3}
|
||||
vectorData := [][]float32{
|
||||
{0.1, 0.2},
|
||||
{0.1, 0.2},
|
||||
{0.1, 0.2},
|
||||
}
|
||||
|
||||
sr := ResultSet{
|
||||
sch: entity.NewSchema().
|
||||
WithField(entity.NewField().WithName("id").WithIsPrimaryKey(true).WithDataType(entity.FieldTypeInt64)).
|
||||
WithField(entity.NewField().WithName("vector").WithDim(2).WithDataType(entity.FieldTypeFloatVector)),
|
||||
IDs: column.NewColumnInt64("id", idData),
|
||||
Fields: DataSet([]column.Column{
|
||||
column.NewColumnFloatVector("vector", 2, vectorData),
|
||||
}),
|
||||
}
|
||||
|
||||
var receiver []*PtrPKData
|
||||
err := sr.Unmarshal(&receiver)
|
||||
s.NoError(err)
|
||||
s.Require().Len(receiver, 3)
|
||||
|
||||
for idx, row := range receiver {
|
||||
s.Require().NotNil(row.A)
|
||||
s.Equal(idData[idx], *row.A)
|
||||
s.Equal(vectorData[idx], row.V)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResults(t *testing.T) {
|
||||
suite.Run(t, new(ResultSetSuite))
|
||||
}
|
||||
|
||||
+32
-3
@@ -138,7 +138,15 @@ func AnyToColumns(rows []interface{}, keepPkField bool, schemas ...*entity.Schem
|
||||
}
|
||||
nameColumns[fieldName] = column
|
||||
|
||||
err = column.AppendValue(candi.v.Interface())
|
||||
if candi.isPtr {
|
||||
if candi.v.IsNil() {
|
||||
err = column.AppendNull()
|
||||
} else {
|
||||
err = column.AppendValue(candi.v.Elem().Interface())
|
||||
}
|
||||
} else {
|
||||
err = column.AppendValue(candi.v.Interface())
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -148,7 +156,15 @@ func AnyToColumns(rows []interface{}, keepPkField bool, schemas ...*entity.Schem
|
||||
if isDynamic {
|
||||
m := make(map[string]interface{})
|
||||
for name, candi := range set {
|
||||
m[name] = candi.v.Interface()
|
||||
if candi.isPtr {
|
||||
if candi.v.IsNil() {
|
||||
m[name] = nil
|
||||
} else {
|
||||
m[name] = candi.v.Elem().Interface()
|
||||
}
|
||||
} else {
|
||||
m[name] = candi.v.Interface()
|
||||
}
|
||||
}
|
||||
bs, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
@@ -312,7 +328,17 @@ func SetField(receiver any, fieldName string, value any) error {
|
||||
}
|
||||
|
||||
if candidate.v.CanSet() {
|
||||
candidate.v.Set(reflect.ValueOf(value))
|
||||
if candidate.isPtr {
|
||||
if value == nil {
|
||||
candidate.v.Set(reflect.Zero(candidate.v.Type()))
|
||||
} else {
|
||||
ptr := reflect.New(candidate.v.Type().Elem())
|
||||
ptr.Elem().Set(reflect.ValueOf(value))
|
||||
candidate.v.Set(ptr)
|
||||
}
|
||||
} else {
|
||||
candidate.v.Set(reflect.ValueOf(value))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -322,6 +348,7 @@ type fieldCandi struct {
|
||||
name string
|
||||
v reflect.Value
|
||||
options map[string]string
|
||||
isPtr bool
|
||||
}
|
||||
|
||||
func reflectValueCandi(v reflect.Value) (map[string]fieldCandi, error) {
|
||||
@@ -400,6 +427,7 @@ func getStructReflectCandidates(v reflect.Value) (map[string]fieldCandi, error)
|
||||
}
|
||||
|
||||
v := v.Field(i)
|
||||
isPtr := v.Kind() == reflect.Ptr
|
||||
if v.Kind() == reflect.Array {
|
||||
v = v.Slice(0, v.Len())
|
||||
}
|
||||
@@ -408,6 +436,7 @@ func getStructReflectCandidates(v reflect.Value) (map[string]fieldCandi, error)
|
||||
name: name,
|
||||
v: v,
|
||||
options: settings,
|
||||
isPtr: isPtr,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +245,161 @@ func (s *RowsSuite) TestReflectValueCandi() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RowsSuite) TestNullablePointerColumns() {
|
||||
s.Run("nil_pointer_appends_null", func() {
|
||||
type NullableRow struct {
|
||||
ID int64 `milvus:"primary_key"`
|
||||
Name *string `milvus:"max_length:256"`
|
||||
Age *int32
|
||||
Vector []float32 `milvus:"dim:16"`
|
||||
}
|
||||
|
||||
columns, err := AnyToColumns([]any{&NullableRow{
|
||||
ID: 1,
|
||||
Name: nil,
|
||||
Age: nil,
|
||||
Vector: make([]float32, 16),
|
||||
}}, false)
|
||||
s.NoError(err)
|
||||
|
||||
for _, col := range columns {
|
||||
if col.Name() == "Name" || col.Name() == "Age" {
|
||||
s.True(col.Nullable(), "column %s should be nullable", col.Name())
|
||||
isNull, err := col.IsNull(0)
|
||||
s.NoError(err)
|
||||
s.True(isNull, "column %s should have null at index 0", col.Name())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
s.Run("non_nil_pointer_appends_value", func() {
|
||||
type NullableRow struct {
|
||||
ID int64 `milvus:"primary_key"`
|
||||
Name *string `milvus:"max_length:256"`
|
||||
Age *int32
|
||||
Vector []float32 `milvus:"dim:16"`
|
||||
}
|
||||
|
||||
name := "test"
|
||||
age := int32(25)
|
||||
columns, err := AnyToColumns([]any{&NullableRow{
|
||||
ID: 1,
|
||||
Name: &name,
|
||||
Age: &age,
|
||||
Vector: make([]float32, 16),
|
||||
}}, false)
|
||||
s.NoError(err)
|
||||
|
||||
for _, col := range columns {
|
||||
switch col.Name() {
|
||||
case "Name":
|
||||
s.True(col.Nullable())
|
||||
isNull, err := col.IsNull(0)
|
||||
s.NoError(err)
|
||||
s.False(isNull)
|
||||
val, err := col.Get(0)
|
||||
s.NoError(err)
|
||||
s.Equal("test", val)
|
||||
case "Age":
|
||||
s.True(col.Nullable())
|
||||
isNull, err := col.IsNull(0)
|
||||
s.NoError(err)
|
||||
s.False(isNull)
|
||||
val, err := col.Get(0)
|
||||
s.NoError(err)
|
||||
s.Equal(int32(25), val)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
s.Run("mixed_nil_and_values", func() {
|
||||
type NullableRow struct {
|
||||
ID int64 `milvus:"primary_key"`
|
||||
Name *string `milvus:"max_length:256"`
|
||||
Vector []float32 `milvus:"dim:16"`
|
||||
}
|
||||
|
||||
name := "hello"
|
||||
columns, err := AnyToColumns([]any{
|
||||
&NullableRow{ID: 1, Name: &name, Vector: make([]float32, 16)},
|
||||
&NullableRow{ID: 2, Name: nil, Vector: make([]float32, 16)},
|
||||
}, false)
|
||||
s.NoError(err)
|
||||
|
||||
for _, col := range columns {
|
||||
if col.Name() == "Name" {
|
||||
s.True(col.Nullable())
|
||||
|
||||
isNull0, err := col.IsNull(0)
|
||||
s.NoError(err)
|
||||
s.False(isNull0)
|
||||
val, err := col.Get(0)
|
||||
s.NoError(err)
|
||||
s.Equal("hello", val)
|
||||
|
||||
isNull1, err := col.IsNull(1)
|
||||
s.NoError(err)
|
||||
s.True(isNull1)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RowsSuite) TestSetFieldPointer() {
|
||||
s.Run("set_pointer_field_with_value", func() {
|
||||
type PtrStruct struct {
|
||||
Name *string
|
||||
}
|
||||
row := &PtrStruct{}
|
||||
err := SetField(row, "Name", "hello")
|
||||
s.NoError(err)
|
||||
s.Require().NotNil(row.Name)
|
||||
s.Equal("hello", *row.Name)
|
||||
})
|
||||
|
||||
s.Run("set_pointer_field_with_nil", func() {
|
||||
type PtrStruct struct {
|
||||
Name *string
|
||||
}
|
||||
name := "old"
|
||||
row := &PtrStruct{Name: &name}
|
||||
err := SetField(row, "Name", nil)
|
||||
s.NoError(err)
|
||||
s.Nil(row.Name)
|
||||
})
|
||||
|
||||
s.Run("set_non_pointer_field", func() {
|
||||
type RegularStruct struct {
|
||||
Name string
|
||||
}
|
||||
row := &RegularStruct{}
|
||||
err := SetField(row, "Name", "hello")
|
||||
s.NoError(err)
|
||||
s.Equal("hello", row.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RowsSuite) TestReflectValueCandiPointer() {
|
||||
s.Run("pointer_field_isPtr", func() {
|
||||
type PtrStruct struct {
|
||||
Name *string
|
||||
Value int64
|
||||
}
|
||||
name := "test"
|
||||
v := reflect.ValueOf(PtrStruct{Name: &name, Value: 42})
|
||||
result, err := reflectValueCandi(v)
|
||||
s.NoError(err)
|
||||
|
||||
nameCandi, ok := result["Name"]
|
||||
s.True(ok)
|
||||
s.True(nameCandi.isPtr)
|
||||
|
||||
valueCandi, ok := result["Value"]
|
||||
s.True(ok)
|
||||
s.False(valueCandi.isPtr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRows(t *testing.T) {
|
||||
suite.Run(t, new(RowsSuite))
|
||||
}
|
||||
|
||||
@@ -65,8 +65,10 @@ func ParseSchema(r interface{}) (*entity.Schema, error) {
|
||||
Name: f.Name,
|
||||
}
|
||||
ft := f.Type
|
||||
if f.Type.Kind() == reflect.Ptr {
|
||||
isPtr := f.Type.Kind() == reflect.Ptr
|
||||
if isPtr {
|
||||
ft = ft.Elem()
|
||||
field.Nullable = true
|
||||
}
|
||||
fv := reflect.New(ft)
|
||||
tag := f.Tag.Get(MilvusTag)
|
||||
@@ -76,6 +78,7 @@ func ParseSchema(r interface{}) (*entity.Schema, error) {
|
||||
tagSettings := ParseTagSetting(tag, MilvusTagSep)
|
||||
if _, has := tagSettings[MilvusPrimaryKey]; has {
|
||||
field.PrimaryKey = true
|
||||
field.Nullable = false
|
||||
}
|
||||
if _, has := tagSettings[MilvusAutoID]; has {
|
||||
field.AutoID = true
|
||||
|
||||
@@ -210,4 +210,61 @@ func TestParseSchema(t *testing.T) {
|
||||
assert.True(t, i64f)
|
||||
assert.True(t, vecf)
|
||||
})
|
||||
|
||||
t.Run("nullable_pointer_fields", func(t *testing.T) {
|
||||
type NullableStruct struct {
|
||||
ID int64 `milvus:"primary_key"`
|
||||
Name *string `milvus:"max_length:256"`
|
||||
Age *int32
|
||||
Score *float64
|
||||
Active *bool
|
||||
Vector []float32 `milvus:"dim:16"`
|
||||
}
|
||||
|
||||
sch, err := ParseSchema(&NullableStruct{})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sch)
|
||||
|
||||
fieldMap := make(map[string]*entity.Field)
|
||||
for _, f := range sch.Fields {
|
||||
fieldMap[f.Name] = f
|
||||
}
|
||||
|
||||
// PK field should NOT be nullable even if it were a pointer (it's not here)
|
||||
assert.False(t, fieldMap["ID"].Nullable)
|
||||
|
||||
// Pointer fields should be nullable
|
||||
assert.True(t, fieldMap["Name"].Nullable)
|
||||
assert.Equal(t, entity.FieldTypeVarChar, fieldMap["Name"].DataType)
|
||||
|
||||
assert.True(t, fieldMap["Age"].Nullable)
|
||||
assert.Equal(t, entity.FieldTypeInt32, fieldMap["Age"].DataType)
|
||||
|
||||
assert.True(t, fieldMap["Score"].Nullable)
|
||||
assert.Equal(t, entity.FieldTypeDouble, fieldMap["Score"].DataType)
|
||||
|
||||
assert.True(t, fieldMap["Active"].Nullable)
|
||||
assert.Equal(t, entity.FieldTypeBool, fieldMap["Active"].DataType)
|
||||
|
||||
// Non-pointer fields should NOT be nullable
|
||||
assert.False(t, fieldMap["Vector"].Nullable)
|
||||
})
|
||||
|
||||
t.Run("pointer_pk_not_nullable", func(t *testing.T) {
|
||||
type PtrPKStruct struct {
|
||||
ID *int64 `milvus:"primary_key"`
|
||||
Vector []float32 `milvus:"dim:16"`
|
||||
}
|
||||
|
||||
sch, err := ParseSchema(&PtrPKStruct{})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sch)
|
||||
|
||||
for _, f := range sch.Fields {
|
||||
if f.Name == "ID" {
|
||||
assert.True(t, f.PrimaryKey)
|
||||
assert.False(t, f.Nullable, "PK field should not be nullable")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user