Skip to content

Commit 2fa3112

Browse files
committed
fix: projection Store overwriting existing YAML slices and maps
1 parent 387805c commit 2fa3112

3 files changed

Lines changed: 281 additions & 0 deletions

File tree

engine/pkg/util/projection/operations.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func Load(target interface{}, accessor Accessor, options LoadOptions) error {
4242
}
4343

4444
// Store writes the values of the fields of the target struct to the accessor.
45+
// Nil slice and map fields are skipped and do not overwrite existing accessor values.
4546
func Store(target interface{}, accessor Accessor, options StoreOptions) error {
4647
return forEachField(target, func(tag *fieldTag, field reflect.Value) error {
4748
if !tag.matchesStore(options) {
@@ -57,6 +58,12 @@ func Store(target interface{}, accessor Accessor, options StoreOptions) error {
5758

5859
accessorValue = field.Elem().Interface()
5960
} else {
61+
if field.Kind() == reflect.Slice || field.Kind() == reflect.Map {
62+
if field.IsNil() {
63+
return nil
64+
}
65+
}
66+
6067
accessorValue = field.Interface()
6168
}
6269

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package projection
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"gitlab.com/postgres-ai/database-lab/v3/pkg/util/ptypes"
9+
)
10+
11+
type mockAccessor struct {
12+
sets []FieldSet
13+
}
14+
15+
func (m *mockAccessor) Set(set FieldSet) error {
16+
m.sets = append(m.sets, set)
17+
return nil
18+
}
19+
20+
func (m *mockAccessor) Get(get FieldGet) (interface{}, error) {
21+
return nil, nil
22+
}
23+
24+
func TestStore_NilSliceSkipsSet(t *testing.T) {
25+
type sliceStruct struct {
26+
Name string `proj:"nested.name"`
27+
Items []interface{} `proj:"nested.items"`
28+
}
29+
30+
s := &sliceStruct{Name: "test"}
31+
acc := &mockAccessor{}
32+
33+
err := Store(s, acc, StoreOptions{})
34+
require.NoError(t, err)
35+
require.Len(t, acc.sets, 1)
36+
require.Equal(t, []string{"nested", "name"}, acc.sets[0].Path)
37+
require.Equal(t, "test", acc.sets[0].Value)
38+
}
39+
40+
func TestStore_NilMapSkipsSet(t *testing.T) {
41+
type mapStruct struct {
42+
Name string `proj:"nested.name"`
43+
Config map[string]interface{} `proj:"nested.config"`
44+
}
45+
46+
s := &mapStruct{Name: "test"}
47+
acc := &mockAccessor{}
48+
49+
err := Store(s, acc, StoreOptions{})
50+
require.NoError(t, err)
51+
require.Len(t, acc.sets, 1)
52+
require.Equal(t, []string{"nested", "name"}, acc.sets[0].Path)
53+
}
54+
55+
func TestStore_EmptyNonNilSliceCallsSet(t *testing.T) {
56+
type sliceStruct struct {
57+
Items []interface{} `proj:"nested.items"`
58+
}
59+
60+
s := &sliceStruct{Items: []interface{}{}}
61+
acc := &mockAccessor{}
62+
63+
err := Store(s, acc, StoreOptions{})
64+
require.NoError(t, err)
65+
require.Len(t, acc.sets, 1)
66+
require.Equal(t, []string{"nested", "items"}, acc.sets[0].Path)
67+
require.Equal(t, ptypes.Slice, acc.sets[0].Type)
68+
}
69+
70+
func TestStore_EmptyNonNilMapCallsSet(t *testing.T) {
71+
type mapStruct struct {
72+
Config map[string]interface{} `proj:"nested.config"`
73+
}
74+
75+
s := &mapStruct{Config: map[string]interface{}{}}
76+
acc := &mockAccessor{}
77+
78+
err := Store(s, acc, StoreOptions{})
79+
require.NoError(t, err)
80+
require.Len(t, acc.sets, 1)
81+
require.Equal(t, []string{"nested", "config"}, acc.sets[0].Path)
82+
require.Equal(t, ptypes.Map, acc.sets[0].Type)
83+
}

engine/pkg/util/projection/store_yaml_test.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ import (
44
"testing"
55

66
"github.com/stretchr/testify/require"
7+
"gopkg.in/yaml.v3"
8+
9+
"gitlab.com/postgres-ai/database-lab/v3/pkg/util/ptypes"
710
)
811

12+
const updatedName = "updated"
13+
914
func TestStoreYaml(t *testing.T) {
1015
r := require.New(t)
1116
s := fullTestStruct()
@@ -28,3 +33,189 @@ func TestStoreYamlNull(t *testing.T) {
2833
// no changes should have been made to the node
2934
requireYamlNullApplied(t, node)
3035
}
36+
37+
func TestStoreYaml_NilFieldBeforeNonNilFields(t *testing.T) {
38+
type nilFirstStruct struct {
39+
Items []interface{} `proj:"nested.items"`
40+
Config map[string]interface{} `proj:"nested.config"`
41+
Name *string `proj:"nested.name"`
42+
}
43+
44+
const yamlData = `
45+
nested:
46+
items:
47+
- "--no-privileges"
48+
config:
49+
key1: value1
50+
name: original
51+
`
52+
53+
node := &yaml.Node{}
54+
err := yaml.Unmarshal([]byte(yamlData), node)
55+
require.NoError(t, err)
56+
57+
newName := updatedName
58+
s := &nilFirstStruct{Name: &newName}
59+
60+
err = StoreYaml(s, node, StoreOptions{})
61+
require.NoError(t, err)
62+
63+
soft, err := NewSoftYaml(node)
64+
require.NoError(t, err)
65+
66+
nameVal, err := soft.Get(FieldGet{Path: []string{"nested", "name"}, Type: ptypes.String})
67+
require.NoError(t, err)
68+
require.Equal(t, updatedName, nameVal)
69+
70+
itemsVal, err := soft.Get(FieldGet{Path: []string{"nested", "items"}, Type: ptypes.Slice})
71+
require.NoError(t, err)
72+
require.Equal(t, []interface{}{"--no-privileges"}, itemsVal)
73+
74+
configVal, err := soft.Get(FieldGet{Path: []string{"nested", "config"}, Type: ptypes.Map})
75+
require.NoError(t, err)
76+
require.Equal(t, map[string]interface{}{"key1": "value1"}, configVal)
77+
}
78+
79+
func TestStoreYaml_EmptyNonNilSliceOverwritesExisting(t *testing.T) {
80+
type sliceStruct struct {
81+
Name *string `proj:"nested.name"`
82+
Items []interface{} `proj:"nested.items"`
83+
}
84+
85+
const yamlWithItems = `
86+
nested:
87+
name: original
88+
items:
89+
- "--no-privileges"
90+
- "--no-owner"
91+
`
92+
93+
node := &yaml.Node{}
94+
err := yaml.Unmarshal([]byte(yamlWithItems), node)
95+
require.NoError(t, err)
96+
97+
newName := updatedName
98+
s := &sliceStruct{Name: &newName, Items: []interface{}{}}
99+
100+
err = StoreYaml(s, node, StoreOptions{})
101+
require.NoError(t, err)
102+
103+
soft, err := NewSoftYaml(node)
104+
require.NoError(t, err)
105+
106+
nameVal, err := soft.Get(FieldGet{Path: []string{"nested", "name"}, Type: ptypes.String})
107+
require.NoError(t, err)
108+
require.Equal(t, updatedName, nameVal)
109+
110+
itemsVal, err := soft.Get(FieldGet{Path: []string{"nested", "items"}, Type: ptypes.Slice})
111+
require.NoError(t, err)
112+
require.Equal(t, []interface{}{}, itemsVal)
113+
}
114+
115+
func TestStoreYaml_EmptyNonNilMapOverwritesExisting(t *testing.T) {
116+
type mapStruct struct {
117+
Name *string `proj:"nested.name"`
118+
Config map[string]interface{} `proj:"nested.config"`
119+
}
120+
121+
const yamlWithMap = `
122+
nested:
123+
name: original
124+
config:
125+
key1: value1
126+
key2: value2
127+
`
128+
129+
node := &yaml.Node{}
130+
err := yaml.Unmarshal([]byte(yamlWithMap), node)
131+
require.NoError(t, err)
132+
133+
newName := updatedName
134+
s := &mapStruct{Name: &newName, Config: map[string]interface{}{}}
135+
136+
err = StoreYaml(s, node, StoreOptions{})
137+
require.NoError(t, err)
138+
139+
soft, err := NewSoftYaml(node)
140+
require.NoError(t, err)
141+
142+
nameVal, err := soft.Get(FieldGet{Path: []string{"nested", "name"}, Type: ptypes.String})
143+
require.NoError(t, err)
144+
require.Equal(t, updatedName, nameVal)
145+
146+
configVal, err := soft.Get(FieldGet{Path: []string{"nested", "config"}, Type: ptypes.Map})
147+
require.NoError(t, err)
148+
require.Equal(t, map[string]interface{}{}, configVal)
149+
}
150+
151+
func TestStoreYaml_NilSlicePreservesExisting(t *testing.T) {
152+
type sliceStruct struct {
153+
Name *string `proj:"nested.name"`
154+
Items []interface{} `proj:"nested.items"`
155+
}
156+
157+
const yamlWithItems = `
158+
nested:
159+
name: original
160+
items:
161+
- "--no-privileges"
162+
- "--no-owner"
163+
`
164+
165+
node := &yaml.Node{}
166+
err := yaml.Unmarshal([]byte(yamlWithItems), node)
167+
require.NoError(t, err)
168+
169+
newName := updatedName
170+
s := &sliceStruct{Name: &newName}
171+
172+
err = StoreYaml(s, node, StoreOptions{})
173+
require.NoError(t, err)
174+
175+
soft, err := NewSoftYaml(node)
176+
require.NoError(t, err)
177+
178+
nameVal, err := soft.Get(FieldGet{Path: []string{"nested", "name"}, Type: ptypes.String})
179+
require.NoError(t, err)
180+
require.Equal(t, updatedName, nameVal)
181+
182+
itemsVal, err := soft.Get(FieldGet{Path: []string{"nested", "items"}, Type: ptypes.Slice})
183+
require.NoError(t, err)
184+
require.Equal(t, []interface{}{"--no-privileges", "--no-owner"}, itemsVal)
185+
}
186+
187+
func TestStoreYaml_NilMapPreservesExisting(t *testing.T) {
188+
type mapStruct struct {
189+
Name *string `proj:"nested.name"`
190+
Config map[string]interface{} `proj:"nested.config"`
191+
}
192+
193+
const yamlWithMap = `
194+
nested:
195+
name: original
196+
config:
197+
key1: value1
198+
key2: value2
199+
`
200+
201+
node := &yaml.Node{}
202+
err := yaml.Unmarshal([]byte(yamlWithMap), node)
203+
require.NoError(t, err)
204+
205+
newName := updatedName
206+
s := &mapStruct{Name: &newName}
207+
208+
err = StoreYaml(s, node, StoreOptions{})
209+
require.NoError(t, err)
210+
211+
soft, err := NewSoftYaml(node)
212+
require.NoError(t, err)
213+
214+
nameVal, err := soft.Get(FieldGet{Path: []string{"nested", "name"}, Type: ptypes.String})
215+
require.NoError(t, err)
216+
require.Equal(t, updatedName, nameVal)
217+
218+
configVal, err := soft.Get(FieldGet{Path: []string{"nested", "config"}, Type: ptypes.Map})
219+
require.NoError(t, err)
220+
require.Equal(t, map[string]interface{}{"key1": "value1", "key2": "value2"}, configVal)
221+
}

0 commit comments

Comments
 (0)