/* 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 }