dim/query/query.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
}