From bb8e1a65c168e17c151996f7af23e1ff4a1f7b41 Mon Sep 17 00:00:00 2001 From: Yaroslav Kirillov Date: Fri, 24 Jul 2026 16:36:28 +0500 Subject: [PATCH] sort keys in clickhouse output map column --- plugin/output/clickhouse/column_map.go | 6 ++-- plugin/output/clickhouse/insanenode.go | 36 ++++++++++++++------- plugin/output/clickhouse/insanenode_test.go | 26 +++++++++++++++ 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/plugin/output/clickhouse/column_map.go b/plugin/output/clickhouse/column_map.go index 9b1130d26..b8daf669e 100644 --- a/plugin/output/clickhouse/column_map.go +++ b/plugin/output/clickhouse/column_map.go @@ -19,16 +19,16 @@ func NewColMapStringString() *ColMapStringString { var _ InsaneColInput = (*ColMapStringString)(nil) func (t *ColMapStringString) Append(node InsaneNode) error { - var m map[string]string + var kvs []proto.KV[string, string] if node != nil { var err error - m, err = node.AsMapStringString() + kvs, err = node.AsKVArray() if err != nil { return fmt.Errorf("converting node to the map of (string,string): %w", err) } } - t.ColMap.Append(m) + t.ColMap.AppendKV(kvs) return nil } diff --git a/plugin/output/clickhouse/insanenode.go b/plugin/output/clickhouse/insanenode.go index f912a7129..8bd439b76 100644 --- a/plugin/output/clickhouse/insanenode.go +++ b/plugin/output/clickhouse/insanenode.go @@ -1,9 +1,11 @@ package clickhouse import ( + "cmp" "errors" "fmt" "net/netip" + "slices" "strconv" "strings" "time" @@ -30,7 +32,7 @@ type InsaneNode interface { AsIPv4() (proto.IPv4, error) AsIPv6() (proto.IPv6, error) AsTime(proto.Precision) (time.Time, error) - AsMapStringString() (map[string]string, error) + AsKVArray() ([]proto.KV[string, string], error) // sorted map[string]string IsNull() bool } @@ -117,7 +119,7 @@ func (s StrictNode) AsStringArray() ([]string, error) { return vals, nil } -func (s StrictNode) AsMapStringString() (map[string]string, error) { +func (s StrictNode) AsKVArray() ([]proto.KV[string, string], error) { if s.StrictNode == nil || s.IsNull() { return nil, nil } @@ -127,22 +129,26 @@ func (s StrictNode) AsMapStringString() (map[string]string, error) { return nil, err } - m := make(map[string]string) + kvs := make([]proto.KV[string, string], 0, len(fields)) for _, f := range fields { k := f.AsString() vNode := f.AsFieldValue() if vNode == nil || vNode.IsNull() { - m[k] = "" + kvs = append(kvs, proto.KV[string, string]{Key: k, Value: ""}) continue } v, err := vNode.MutateToStrict().AsString() if err != nil { return nil, err } - m[k] = v + kvs = append(kvs, proto.KV[string, string]{Key: k, Value: v}) } - return m, nil + slices.SortStableFunc(kvs, func(a, b proto.KV[string, string]) int { + return cmp.Compare(a.Key, b.Key) + }) + + return kvs, nil } type NonStrictNode struct { @@ -246,19 +252,25 @@ func (n NonStrictNode) AsTime(prec proto.Precision) (time.Time, error) { return t, nil } -func (n NonStrictNode) AsMapStringString() (map[string]string, error) { +func (n NonStrictNode) AsKVArray() ([]proto.KV[string, string], error) { if n.Node == nil || n.Node.IsNull() || !n.IsObject() { return nil, nil } - m := make(map[string]string) - for _, f := range n.AsFields() { + fields := n.AsFields() + + kvs := make([]proto.KV[string, string], 0, len(fields)) + for _, f := range fields { k := f.AsString() v := nonStrictAsString(f.AsFieldValue()) - m[k] = v + kvs = append(kvs, proto.KV[string, string]{Key: k, Value: v}) } - return m, nil + slices.SortStableFunc(kvs, func(a, b proto.KV[string, string]) int { + return cmp.Compare(a.Key, b.Key) + }) + + return kvs, nil } // ZeroValueNode returns a null-value for all called methods. @@ -318,7 +330,7 @@ func (z ZeroValueNode) IsNull() bool { return false } -func (z ZeroValueNode) AsMapStringString() (map[string]string, error) { +func (z ZeroValueNode) AsKVArray() ([]proto.KV[string, string], error) { return nil, nil } diff --git a/plugin/output/clickhouse/insanenode_test.go b/plugin/output/clickhouse/insanenode_test.go index 84fe954fc..6f1d2ef6b 100644 --- a/plugin/output/clickhouse/insanenode_test.go +++ b/plugin/output/clickhouse/insanenode_test.go @@ -89,6 +89,32 @@ func TestNodeAsTime(t *testing.T) { test(`"1740643609"`, proto.PrecisionSecond, mustParseTime("2025-02-27T08:06:49.00Z")) } +func TestNodeAsKVArray(t *testing.T) { + r := require.New(t) + + root, err := insaneJSON.DecodeString(`{"b":"1","c":"2","b":"3","a":"4"}`) + defer insaneJSON.Release(root) + r.NoError(err) + + // expected sorted keys + want := []proto.KV[string, string]{ + {Key: "a", Value: "4"}, + {Key: "b", Value: "1"}, + {Key: "b", Value: "3"}, + {Key: "c", Value: "2"}, + } + + n := NonStrictNode{root.Node} + got1, err := n.AsKVArray() + r.NoError(err) + r.Equal(want, got1) + + sn := StrictNode{root.MutateToStrict()} + got2, err := sn.AsKVArray() + r.NoError(err) + r.Equal(want, got2) +} + func mustParseTime(s string) time.Time { t, err := time.Parse(time.RFC3339Nano, s) if err != nil {