fix: add ValidData to auto-generated $meta field for partial update (#47967)

## Summary
- Fix partial update failure when `enable_dynamic_field=true` but upsert
data contains only static fields (no dynamic fields)
- Root cause: PR #46419 changed `$meta` field schema to `Nullable: true`
+ `DefaultValue: "{}"`. After this change, `autoGenDynamicFieldData`
generates `$meta` data without `ValidData`, so it cannot enter the
`FillWithDefaultValue` branch in `queryPreExecute` (line 312-323 of
`task_upsert.go`), leaving data in compressed format instead of the FULL
format expected by the merge logic.
- Fix: add all-true `ValidData` to auto-generated `$meta` field data,
consistent with its nullable+default-value schema definition
- Add e2e test `TestPartialUpdateDynamicSchemaStaticOnly` covering the
exact scenario

issue: #47957

## Test plan
- [x] E2e test `TestPartialUpdateDynamicSchemaStaticOnly` passed locally
against standalone Milvus
- [x] Unit test `Test_CheckDynamicFieldData` passed (6/6 subtests)
- [x] `make build-go` passed
- [x] `make lint-fix` passed

Signed-off-by: Wei Liu <wei.liu@zilliz.com>
This commit is contained in:
wei liu
2026-03-06 12:31:24 +08:00
committed by GitHub
parent 5748d8df4f
commit fd50c35167
2 changed files with 109 additions and 0 deletions
+5
View File
@@ -1184,6 +1184,10 @@ func autoGenPrimaryFieldData(fieldSchema *schemapb.FieldSchema, data interface{}
}
func autoGenDynamicFieldData(data [][]byte) *schemapb.FieldData {
validData := make([]bool, len(data))
for i := range validData {
validData[i] = true
}
return &schemapb.FieldData{
FieldName: common.MetaFieldName,
Type: schemapb.DataType_JSON,
@@ -1197,6 +1201,7 @@ func autoGenDynamicFieldData(data [][]byte) *schemapb.FieldData {
},
},
IsDynamic: true,
ValidData: validData,
}
}
+104
View File
@@ -125,6 +125,110 @@ func TestPartialUpdateDynamicField(t *testing.T) {
common.EqualColumn(t, hp.MergeColumnsToDynamic(1, []column.Column{dynamicColumnA, dynamicColumnB}, common.DefaultDynamicFieldName), resSet.GetColumn(common.DefaultDynamicFieldName))
}
func TestPartialUpdateDynamicSchemaStaticOnly(t *testing.T) {
/*
Test partial upsert with dynamic schema enabled but data contains ONLY static fields:
1. Create collection with enable_dynamic_field=true, schema has id + vector + name(nullable)
2. Insert initial data with only static fields (no dynamic data)
3. Partial upsert a mixed batch: existing rows + new rows, all with only static fields
4. Verify that existing rows are updated and new rows are inserted correctly
This reproduces a bug where $meta valid_data length mismatches when:
- enable_dynamic_field=true
- partial_update=true
- data contains NO dynamic fields
- batch mixes existing and new primary keys
*/
ctx := hp.CreateContext(t, time.Second*common.DefaultTimeout)
mc := hp.CreateDefaultMilvusClient(ctx, t)
collName := common.GenRandomString("update_dyn_static", 6)
// Create schema: id(pk) + vector + name(nullable), enable_dynamic_field=true
pkField := entity.NewField().WithName(common.DefaultInt64FieldName).WithDataType(entity.FieldTypeInt64).WithIsPrimaryKey(true)
vecField := entity.NewField().WithName(common.DefaultFloatVecFieldName).WithDataType(entity.FieldTypeFloatVector).WithDim(common.DefaultDim)
nameField := entity.NewField().WithName("name").WithDataType(entity.FieldTypeVarChar).WithMaxLength(256).WithNullable(true)
fields := []*entity.Field{pkField, vecField, nameField}
schema := hp.GenSchema(hp.TNewSchemaOption().
TWithName(collName).
TWithDescription("test partial update with dynamic schema and static-only data").
TWithFields(fields).
TWithEnableDynamicField(true))
err := mc.CreateCollection(ctx, client.NewCreateCollectionOption(collName, schema))
common.CheckErr(t, err, true)
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second*10)
defer cancel()
err := mc.DropCollection(ctx, client.NewDropCollectionOption(collName))
common.CheckErr(t, err, true)
})
// Insert initial data: 10 rows with ONLY static fields (no dynamic data at all)
initNb := 10
initPks := make([]int64, initNb)
initNames := make([]string, initNb)
for i := 0; i < initNb; i++ {
initPks[i] = int64(i)
initNames[i] = fmt.Sprintf("item_%d", i)
}
initPkCol := column.NewColumnInt64(common.DefaultInt64FieldName, initPks)
initVecCol := hp.GenColumnData(initNb, entity.FieldTypeFloatVector, *hp.TNewDataOption())
initNameCol := column.NewColumnVarChar("name", initNames)
_, err = mc.Insert(ctx, client.NewColumnBasedInsertOption(collName).WithColumns(initPkCol, initVecCol, initNameCol))
common.CheckErr(t, err, true)
// Flush -> Index -> Load
prepare := &hp.CollectionPrepare{}
prepare.FlushData(ctx, t, mc, collName)
prepare.CreateIndex(ctx, t, mc, hp.TNewIndexParams(schema))
prepare.Load(ctx, t, mc, hp.NewLoadParams(collName))
time.Sleep(time.Second * 3)
// Partial upsert: mixed batch with existing rows (id=0,1) and new rows (id=800,801)
// All rows contain ONLY static fields, no dynamic data
upsertPks := []int64{0, 1, 800, 801}
upsertNames := []string{"static_upd_0", "static_upd_1", "static_new_800", "static_new_801"}
upsertNb := len(upsertPks)
upsertPkCol := column.NewColumnInt64(common.DefaultInt64FieldName, upsertPks)
upsertVecCol := hp.GenColumnData(upsertNb, entity.FieldTypeFloatVector, *hp.TNewDataOption().TWithStart(500))
upsertNameCol := column.NewColumnVarChar("name", upsertNames)
upsertRes, err := mc.Upsert(ctx, client.NewColumnBasedInsertOption(collName).
WithColumns(upsertPkCol, upsertVecCol, upsertNameCol).
WithPartialUpdate(true))
common.CheckErr(t, err, true)
require.EqualValues(t, upsertNb, upsertRes.UpsertCount)
// Query and verify
resSet, err := mc.Query(ctx, client.NewQueryOption(collName).
WithFilter(fmt.Sprintf("%s in [0, 1, 800, 801]", common.DefaultInt64FieldName)).
WithOutputFields(common.DefaultInt64FieldName, "name").
WithConsistencyLevel(entity.ClStrong))
common.CheckErr(t, err, true)
// Build result map by id
require.Equal(t, 4, resSet.GetColumn(common.DefaultInt64FieldName).Len())
pkResults := resSet.GetColumn(common.DefaultInt64FieldName).(*column.ColumnInt64).Data()
nameResults := resSet.GetColumn("name").(*column.ColumnVarChar).Data()
resultMap := make(map[int64]string)
for i, pk := range pkResults {
resultMap[pk] = nameResults[i]
}
// Verify existing rows are updated
require.Equal(t, "static_upd_0", resultMap[0], "id=0 should be updated")
require.Equal(t, "static_upd_1", resultMap[1], "id=1 should be updated")
// Verify new rows are inserted
require.Equal(t, "static_new_800", resultMap[800], "id=800 should be inserted")
require.Equal(t, "static_new_801", resultMap[801], "id=801 should be inserted")
}
func TestUpdateNullableFieldBehavior(t *testing.T) {
/*
Test nullable field behavior for Update operation: