From 88c271667ba7d95d9d0667c9952ece4812a1ffee Mon Sep 17 00:00:00 2001 From: Gibheer Date: Fri, 7 May 2021 21:03:58 +0200 Subject: [PATCH] add function FieldsToJSON This function enables the show view to be much less write overhead. By defining which columns to return and automatically merging the attributes into the main view, this can be made so much easier. It doesn't support the recursive view for now, so that is something a client would need to handle, but for now this should be good enough. This also fixes a small issue in the update clause handler by moving the index handler into the handler when a column was found. If that was not done, the index gets moved to the wrong position and the resulting query would be wrongly indexed. --- query/query.go | 14 ++++++++++++- query/query_test.go | 48 +++++++++++++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/query/query.go b/query/query.go index 5d1ab98..cf567c0 100644 --- a/query/query.go +++ b/query/query.go @@ -36,6 +36,18 @@ func FieldListToSelect(tableName string, fl types.FieldList, nameMap map[string] return strings.Join(res, ",") } +// FieldsToJSON converts a map of field names to columns to build a json output. +// The attributes column of the table is merged into the resulting JSON. +// Fields must contain the name of the resulting field and the column name where the value +// is coming from. +func FieldsToJSON(table string, fields map[string]string) string { + res := []string{} + for key, val := range fields { + res = append(res, fmt.Sprintf("'%s', %s", key, val)) + } + return fmt.Sprintf("jsonb_build_object(%s) || attributes", strings.Join(res, ",")) +} + // nameToAttrPath takes a dotted string and converts it into a json field path. func nameToAttrPath(tabName, name string) string { if name == "" { @@ -67,8 +79,8 @@ func FieldMapToUpdate(fm types.FieldMap, nameMap map[string]string) (string, []i attrVals := map[string]interface{}{} i := 0 for key, val := range fm.Fields() { - i++ if name, found := nameMap[key]; found { + i++ setClause = append(setClause, fmt.Sprintf("%s = $%d", name, i)) if val == "" { args = append(args, nil) diff --git a/query/query_test.go b/query/query_test.go index 94cbcfa..33519a4 100644 --- a/query/query_test.go +++ b/query/query_test.go @@ -13,18 +13,23 @@ func TestFieldListToSelect(t *testing.T) { mapping map[string]string out string }{ - {"foo", types.NewFieldList("name"), map[string]string{"name": "name"}, "name as name"}, + { + "foo", + types.NewFieldList("name"), + map[string]string{"name": "name"}, + `name as "name"`, + }, { "foo", types.NewFieldList("foo", "bar", "baz"), map[string]string{"foo": "c.foo", "bar": "b.bar"}, - "b.bar as bar,foo.attributes->'baz' as baz,c.foo as foo", + `b.bar as "bar",foo.attributes->>'baz' as "baz",c.foo as "foo"`, }, { "foo", types.NewFieldList("subnets", "vlan", "name"), map[string]string{"subnets": "s.subnets", "vlan": "vlan.vlan_id", "name": "p.name"}, - "p.name as name,s.subnets as subnets,vlan.vlan_id as vlan", + `p.name as "name",s.subnets as "subnets",vlan.vlan_id as "vlan"`, }, } @@ -36,14 +41,28 @@ func TestFieldListToSelect(t *testing.T) { } } +func TestJSONSelect(t *testing.T) { + tests := []struct { + table string + fields map[string]string + out string + }{} + for _, test := range tests { + out := FieldsToJSON(test.table, test.fields) + if out != test.out { + t.Errorf("expected `%s`, got `%s`", test.out, out) + } + } +} + func TestNameToAttrPath(t *testing.T) { tests := []struct { table string in string out string }{ - {"zoo", "foo", `zoo.attributes->'foo'`}, - {"zoo", "foo.bar", `zoo.attributes->'foo'->'bar'`}, + {"zoo", "foo", `zoo.attributes->>'foo'`}, + {"zoo", "foo.bar", `zoo.attributes->'foo'->>'bar'`}, {"zoo", "", `zoo.attributes`}, } @@ -57,37 +76,36 @@ func TestNameToAttrPath(t *testing.T) { func TestFieldMapToUpdate(t *testing.T) { tests := []struct { - table string vals types.FieldMap mapping map[string]string set string // expected set clause args []interface{} // expected arguments }{ { // check for normal field mapping - "zoo", types.NewFieldMap(map[string]interface{}{"key": "value"}), map[string]string{"key": "field"}, - "zoo.field = $1", + "field = $1", []interface{}{"value"}, }, { // generate attributes field - "zoo", types.NewFieldMap(map[string]interface{}{"key2": "value"}), map[string]string{"key": "field"}, - "zoo.attributes->'key2' = $1", - []interface{}{"value"}, + "attributes = jsonb_strip_nulls(attributes || $1::jsonb)", + []interface{}{`{"key2":"value"}`}, }, { // mixed mapped and unmapped field - "zoo", types.NewFieldMap(map[string]interface{}{"key2": "value", "key": "value"}), map[string]string{"key": "field"}, - "zoo.attributes->'key2' = $1,zoo.field = $2", - []interface{}{"value", "value"}, + "field = $1,attributes = jsonb_strip_nulls(attributes || $2::jsonb)", + []interface{}{"value", `{"key2":"value"}`}, }, } for _, test := range tests { - set, args := FieldMapToUpdate(test.table, test.vals, test.mapping) + set, args, err := FieldMapToUpdate(test.vals, test.mapping) + if err != nil { + t.Errorf("could not map to update: %s", err) + } if set != test.set { t.Errorf("expected set clause `%s`, got `%s`", test.set, set) }