135 lines
4.2 KiB
Go
135 lines
4.2 KiB
Go
/*
|
|
Package query provides functions useful for building database queries.
|
|
|
|
It contains functions to build select clauses or where clauses together
|
|
with the necessary parameter keys.
|
|
*/
|
|
package query
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"dim/types"
|
|
)
|
|
|
|
// FieldListToSelect converts the fieldlist to a select clause.
|
|
//
|
|
// It takes a fieldField and tries to find a matching name in the nameMap and
|
|
// uses the provided name.
|
|
// Any field that is not found will be converted to an attributes selector, so
|
|
// that extra attributes can be selected dynamically.
|
|
// Each field will be selected with the requested name, so be careful to
|
|
// avoid collisions.
|
|
// The tableName will be used as a prefix to the attributes field. Only one
|
|
// attributes field can be used at the same time.
|
|
func FieldListToSelect(tableName string, fl types.FieldList, nameMap map[string]string) string {
|
|
res := []string{}
|
|
for _, name := range fl.Fields() {
|
|
if field, found := nameMap[name]; found {
|
|
res = append(res, fmt.Sprintf(`%s as "%s"`, field, name))
|
|
} else {
|
|
res = append(res, fmt.Sprintf(`%s as "%s"`, nameToAttrPath(tableName, name), name))
|
|
}
|
|
}
|
|
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) || %s.attributes", strings.Join(res, ","), table)
|
|
}
|
|
|
|
// nameToAttrPath takes a dotted string and converts it into a json field path.
|
|
func nameToAttrPath(tabName, name string) string {
|
|
if name == "" {
|
|
return tabName + ".attributes"
|
|
}
|
|
parts := strings.Split(name, ".")
|
|
for i, part := range parts {
|
|
parts[i] = fmt.Sprintf(`'%s'`, part)
|
|
}
|
|
path := fmt.Sprintf("%s.attributes", tabName)
|
|
if len(parts) > 1 {
|
|
path += fmt.Sprintf("->%s", strings.Join(parts[:len(parts)-1], "->"))
|
|
}
|
|
path += fmt.Sprintf("->>%s", parts[len(parts)-1])
|
|
return path
|
|
}
|
|
|
|
// FieldMapToUpdate generates the necessary elements for an update.
|
|
//
|
|
// It returns the set clause for the update statement and the arguments for the placeholders.
|
|
// The index will start with 1, so every other parameter not included in the update needs to
|
|
// use the size of the field map + 1 as the next index.
|
|
// If the key points is not found in the nameMap, the value will be joined with the attributes
|
|
// column of the table.
|
|
// An error is returned when the attribute values can't be encoded correctly.
|
|
func FieldMapToUpdate(fm types.FieldMap, nameMap map[string]string) (string, []interface{}, error) {
|
|
setClause := []string{}
|
|
args := []interface{}{}
|
|
attrVals := map[string]interface{}{}
|
|
i := 0
|
|
for key, val := range fm.Fields() {
|
|
if name, found := nameMap[key]; found {
|
|
if name == "" {
|
|
continue
|
|
}
|
|
i++
|
|
setClause = append(setClause, fmt.Sprintf("%s = $%d", name, i))
|
|
if val == "" {
|
|
args = append(args, nil)
|
|
} else {
|
|
args = append(args, val)
|
|
}
|
|
} else {
|
|
parts := strings.Split(key, ".")
|
|
attrVals = setJSONPath(attrVals, parts, val)
|
|
}
|
|
}
|
|
if len(attrVals) > 0 {
|
|
setClause = append(
|
|
setClause,
|
|
fmt.Sprintf("attributes = jsonb_strip_nulls(attributes || $%d::jsonb)", len(args)+1),
|
|
)
|
|
raw, err := json.Marshal(attrVals)
|
|
if err != nil {
|
|
return "", []interface{}{}, fmt.Errorf("could not encode attributes: %#v", err)
|
|
}
|
|
args = append(args, string(raw))
|
|
}
|
|
return strings.Join(setClause, ","), args, nil
|
|
}
|
|
|
|
// Set a value to a nested map structure.
|
|
// The path must be a list of steps to traverse the map structure.
|
|
func setJSONPath(target map[string]interface{}, path []string, val interface{}) map[string]interface{} {
|
|
res := target
|
|
if len(path) > 1 {
|
|
raw, found := res[path[0]]
|
|
|
|
if !found {
|
|
res[path[0]] = map[string]interface{}{}
|
|
raw = res[path[0]]
|
|
} else {
|
|
values, worked := raw.(map[string]interface{})
|
|
if !worked {
|
|
values = map[string]interface{}{}
|
|
res[path[0]] = values
|
|
}
|
|
}
|
|
res[path[0]] = setJSONPath(res[path[0]].(map[string]interface{}), path[1:], val)
|
|
return res
|
|
}
|
|
res[path[0]] = val
|
|
return res
|
|
}
|