Skip to content

Commit 2d72850

Browse files
Merge pull request #1482 from ClickHouse/json_nested_map_fix
fix: JSON NestedMap + add tests
2 parents 1195999 + 78e787d commit 2d72850

File tree

2 files changed

+104
-7
lines changed

2 files changed

+104
-7
lines changed

lib/chcol/json.go

+20-7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"database/sql/driver"
2222
"encoding/json"
2323
"fmt"
24+
"slices"
2425
"strings"
2526
)
2627

@@ -51,11 +52,22 @@ func (o *JSON) ValueAtPath(path string) (any, bool) {
5152

5253
// NestedMap converts the flattened JSON data into a nested structure
5354
func (o *JSON) NestedMap() map[string]any {
54-
nested := make(map[string]any)
55+
result := make(map[string]any)
5556

56-
for key, value := range o.valuesByPath {
57-
parts := strings.Split(key, ".")
58-
current := nested
57+
sortedPaths := make([]string, 0, len(o.valuesByPath))
58+
for path := range o.valuesByPath {
59+
sortedPaths = append(sortedPaths, path)
60+
}
61+
slices.Sort(sortedPaths)
62+
63+
for _, path := range sortedPaths {
64+
value := o.valuesByPath[path]
65+
if vt, ok := value.(Variant); ok && vt.Nil() {
66+
continue
67+
}
68+
69+
parts := strings.Split(path, ".")
70+
current := result
5971

6072
for i := 0; i < len(parts)-1; i++ {
6173
part := parts[i]
@@ -64,13 +76,14 @@ func (o *JSON) NestedMap() map[string]any {
6476
current[part] = make(map[string]any)
6577
}
6678

67-
current = current[part].(map[string]any)
79+
if next, ok := current[part].(map[string]any); ok {
80+
current = next
81+
}
6882
}
69-
7083
current[parts[len(parts)-1]] = value
7184
}
7285

73-
return nested
86+
return result
7487
}
7588

7689
// MarshalJSON implements the json.Marshaler interface

lib/chcol/json_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package chcol
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestNestedMap(t *testing.T) {
10+
cases := []struct {
11+
name string
12+
input *JSON
13+
expected map[string]any
14+
}{
15+
{
16+
name: "nested object with values present",
17+
input: &JSON{
18+
valuesByPath: map[string]any{
19+
"x": NewVariant(nil),
20+
"x.a": NewVariant(42),
21+
"x.b": NewVariant(64),
22+
"x.b.c.d": NewVariant(96),
23+
"a.b.c": NewVariant(128),
24+
},
25+
},
26+
expected: map[string]any{
27+
"x": map[string]any{
28+
"a": NewVariant(42),
29+
"b": NewVariant(64),
30+
"c": map[string]any{
31+
"d": NewVariant(96),
32+
},
33+
},
34+
"a": map[string]any{
35+
"b": map[string]any{
36+
"c": NewVariant(128),
37+
},
38+
},
39+
},
40+
},
41+
{
42+
name: "nested object with only top level path present",
43+
input: &JSON{
44+
valuesByPath: map[string]any{
45+
"x": NewVariant(42),
46+
"x.a": NewVariant(nil),
47+
"x.b": NewVariant(nil),
48+
"x.b.c.d": NewVariant(nil),
49+
"a.b.c": NewVariant(nil),
50+
},
51+
},
52+
expected: map[string]any{
53+
"x": NewVariant(42),
54+
},
55+
},
56+
{
57+
name: "nested object with typed paths",
58+
input: &JSON{
59+
valuesByPath: map[string]any{
60+
"x": 42,
61+
"a.b": "test value",
62+
},
63+
},
64+
expected: map[string]any{
65+
"x": 42,
66+
"a": map[string]any{
67+
"b": "test value",
68+
},
69+
},
70+
},
71+
{
72+
name: "empty object",
73+
input: NewJSON(),
74+
expected: map[string]any{},
75+
},
76+
}
77+
78+
for _, c := range cases {
79+
t.Run(c.name, func(t *testing.T) {
80+
actual := c.input.NestedMap()
81+
require.Equal(t, c.expected, actual)
82+
})
83+
}
84+
}

0 commit comments

Comments
 (0)