Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions internal/concat.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,18 +137,30 @@ func concatMaps(ms reflect.Value) (reflect.Value, error) {
vals := rms.MapIndex(key)

anyVals := vals.Interface().([]any)
if len(anyVals) == 1 {
ele := anyVals[0]
if ele == nil { // we cannot SetMapIndex with nil because it will delete the key
ret.SetMapIndex(key, reflect.Zero(typ.Elem()))
continue

// nil represents an absent/empty value for this key in a chunk.
// Filter nils out so concat is order-independent and toSliceValue
// never receives a nil element (reflect.TypeOf(nil) returns a nil
// reflect.Type, which makes reflect.SliceOf(nil) panic).
nonNil := make([]any, 0, len(anyVals))
for _, v := range anyVals {
if v != nil {
nonNil = append(nonNil, v)
}
}

ret.SetMapIndex(key, reflect.ValueOf(ele))
switch len(nonNil) {
case 0:
// all chunks had a nil value for this key; preserve the key with a zero value.
// we cannot SetMapIndex with nil because it will delete the key.
ret.SetMapIndex(key, reflect.Zero(typ.Elem()))
continue
case 1:
ret.SetMapIndex(key, reflect.ValueOf(nonNil[0]))
continue
}

v, err := toSliceValue(anyVals)
v, err := toSliceValue(nonNil)
if err != nil {
return reflect.Value{}, err
}
Expand Down
58 changes: 58 additions & 0 deletions internal/concat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,62 @@ func TestConcat(t *testing.T) {
},
}, m)
})

// nil and non-nil for the same key across chunks must concat order-
// independently and must never panic. See issue #1181.
t.Run("concat same key: nil then value does not panic", func(t *testing.T) {
m, err := ConcatItems([]map[string]any{
{"a": nil},
{"a": "str"},
})
assert.Nil(t, err)
assert.Equal(t, map[string]any{"a": "str"}, m)
})

t.Run("concat same key: value then nil is order-independent", func(t *testing.T) {
m, err := ConcatItems([]map[string]any{
{"a": "str"},
{"a": nil},
})
assert.Nil(t, err)
assert.Equal(t, map[string]any{"a": "str"}, m)
})

t.Run("concat same key: all nil preserves nil", func(t *testing.T) {
m, err := ConcatItems([]map[string]any{
{"a": nil},
{"a": nil},
})
assert.Nil(t, err)
assert.Equal(t, map[string]any{"a": nil}, m)
})

t.Run("concat same key: nil mixed with multiple non-nil strings", func(t *testing.T) {
m, err := ConcatItems([]map[string]any{
{"a": nil},
{"a": "foo"},
{"a": "bar"},
{"a": nil},
})
assert.Nil(t, err)
assert.Equal(t, map[string]any{"a": "foobar"}, m)
})

t.Run("concat same key: nested map with nil then value", func(t *testing.T) {
m, err := ConcatItems([]map[string]any{
{"a": map[string]any{"x": nil}},
{"a": map[string]any{"x": "v"}},
})
assert.Nil(t, err)
assert.Equal(t, map[string]any{"a": map[string]any{"x": "v"}}, m)
})

t.Run("concat same key: nested map all nil preserves nil", func(t *testing.T) {
m, err := ConcatItems([]map[string]any{
{"a": map[string]any{"x": nil}},
{"a": map[string]any{"x": nil}},
})
assert.Nil(t, err)
assert.Equal(t, map[string]any{"a": map[string]any{"x": nil}}, m)
})
}