/* 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, ",") } // 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) } return fmt.Sprintf("%s.attributes->%s", tabName, strings.Join(parts, "->")) } // 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() { i++ if name, found := nameMap[key]; found { 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 }