Skip to content

Commit

Permalink
Merge pull request #1482 from ClickHouse/json_nested_map_fix
Browse files Browse the repository at this point in the history
fix: JSON NestedMap + add tests
  • Loading branch information
SpencerTorres authored Jan 28, 2025
2 parents 1195999 + 78e787d commit 2d72850
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 7 deletions.
27 changes: 20 additions & 7 deletions lib/chcol/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"database/sql/driver"
"encoding/json"
"fmt"
"slices"
"strings"
)

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

// NestedMap converts the flattened JSON data into a nested structure
func (o *JSON) NestedMap() map[string]any {
nested := make(map[string]any)
result := make(map[string]any)

for key, value := range o.valuesByPath {
parts := strings.Split(key, ".")
current := nested
sortedPaths := make([]string, 0, len(o.valuesByPath))
for path := range o.valuesByPath {
sortedPaths = append(sortedPaths, path)
}
slices.Sort(sortedPaths)

for _, path := range sortedPaths {
value := o.valuesByPath[path]
if vt, ok := value.(Variant); ok && vt.Nil() {
continue
}

parts := strings.Split(path, ".")
current := result

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

current = current[part].(map[string]any)
if next, ok := current[part].(map[string]any); ok {
current = next
}
}

current[parts[len(parts)-1]] = value
}

return nested
return result
}

// MarshalJSON implements the json.Marshaler interface
Expand Down
84 changes: 84 additions & 0 deletions lib/chcol/json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package chcol

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestNestedMap(t *testing.T) {
cases := []struct {
name string
input *JSON
expected map[string]any
}{
{
name: "nested object with values present",
input: &JSON{
valuesByPath: map[string]any{
"x": NewVariant(nil),
"x.a": NewVariant(42),
"x.b": NewVariant(64),
"x.b.c.d": NewVariant(96),
"a.b.c": NewVariant(128),
},
},
expected: map[string]any{
"x": map[string]any{
"a": NewVariant(42),
"b": NewVariant(64),
"c": map[string]any{
"d": NewVariant(96),
},
},
"a": map[string]any{
"b": map[string]any{
"c": NewVariant(128),
},
},
},
},
{
name: "nested object with only top level path present",
input: &JSON{
valuesByPath: map[string]any{
"x": NewVariant(42),
"x.a": NewVariant(nil),
"x.b": NewVariant(nil),
"x.b.c.d": NewVariant(nil),
"a.b.c": NewVariant(nil),
},
},
expected: map[string]any{
"x": NewVariant(42),
},
},
{
name: "nested object with typed paths",
input: &JSON{
valuesByPath: map[string]any{
"x": 42,
"a.b": "test value",
},
},
expected: map[string]any{
"x": 42,
"a": map[string]any{
"b": "test value",
},
},
},
{
name: "empty object",
input: NewJSON(),
expected: map[string]any{},
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual := c.input.NestedMap()
require.Equal(t, c.expected, actual)
})
}
}

0 comments on commit 2d72850

Please sign in to comment.