2016-09-15 05:45:09 +02:00
|
|
|
package queries
|
2016-05-17 12:00:56 +02:00
|
|
|
|
|
|
|
import (
|
2016-08-13 20:42:28 +02:00
|
|
|
"database/sql"
|
2016-05-17 12:00:56 +02:00
|
|
|
"fmt"
|
|
|
|
"reflect"
|
2016-08-08 02:11:45 +02:00
|
|
|
"strings"
|
2016-08-31 09:47:35 +02:00
|
|
|
"sync"
|
2016-05-17 12:00:56 +02:00
|
|
|
|
2016-08-08 02:11:45 +02:00
|
|
|
"github.com/pkg/errors"
|
2016-09-15 05:45:09 +02:00
|
|
|
"github.com/vattle/sqlboiler/boil"
|
2016-08-09 09:59:30 +02:00
|
|
|
"github.com/vattle/sqlboiler/strmangle"
|
2016-05-17 12:00:56 +02:00
|
|
|
)
|
|
|
|
|
2016-08-08 02:11:45 +02:00
|
|
|
var (
|
|
|
|
bindAccepts = []reflect.Kind{reflect.Ptr, reflect.Slice, reflect.Ptr, reflect.Struct}
|
|
|
|
|
2016-09-01 06:18:30 +02:00
|
|
|
mut sync.RWMutex
|
|
|
|
bindingMaps = make(map[string][]uint64)
|
2016-09-03 20:37:56 +02:00
|
|
|
structMaps = make(map[string]map[string]uint64)
|
2016-09-01 06:18:30 +02:00
|
|
|
)
|
|
|
|
|
2016-09-03 19:33:28 +02:00
|
|
|
// Identifies what kind of object we're binding to
|
|
|
|
type bindKind int
|
|
|
|
|
|
|
|
const (
|
|
|
|
kindStruct bindKind = iota
|
|
|
|
kindSliceStruct
|
|
|
|
kindPtrSliceStruct
|
|
|
|
)
|
|
|
|
|
2016-08-30 07:21:32 +02:00
|
|
|
const (
|
|
|
|
loadMethodPrefix = "Load"
|
|
|
|
relationshipStructName = "R"
|
2016-09-02 03:01:20 +02:00
|
|
|
loaderStructName = "L"
|
2016-09-01 06:11:42 +02:00
|
|
|
sentinel = uint64(255)
|
2016-08-30 07:21:32 +02:00
|
|
|
)
|
|
|
|
|
2016-08-08 02:11:45 +02:00
|
|
|
// BindP executes the query and inserts the
|
|
|
|
// result into the passed in object pointer.
|
2016-08-12 07:57:07 +02:00
|
|
|
// It panics on error. See boil.Bind() documentation.
|
2016-08-08 02:11:45 +02:00
|
|
|
func (q *Query) BindP(obj interface{}) {
|
|
|
|
if err := q.Bind(obj); err != nil {
|
2016-09-15 05:45:09 +02:00
|
|
|
panic(boil.WrapErr(err))
|
2016-08-08 02:11:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-17 12:00:56 +02:00
|
|
|
// Bind executes the query and inserts the
|
|
|
|
// result into the passed in object pointer
|
2016-08-08 02:11:45 +02:00
|
|
|
//
|
|
|
|
// Bind rules:
|
2016-08-18 03:52:42 +02:00
|
|
|
// - Struct tags control bind, in the form of: `boil:"name,bind"`
|
2016-08-23 05:14:46 +02:00
|
|
|
// - If "name" is omitted the sql column names that come back are TitleCased
|
|
|
|
// and matched against the field name.
|
|
|
|
// - If the "name" part of the struct tag is specified, the given name will
|
|
|
|
// be used instead of the struct field name for binding.
|
|
|
|
// - If the "name" of the struct tag is "-", this field will not be bound to.
|
|
|
|
// - If the ",bind" option is specified on a struct field and that field
|
|
|
|
// is a struct itself, it will be recursed into to look for fields for binding.
|
2016-08-08 02:11:45 +02:00
|
|
|
//
|
|
|
|
// Example Query:
|
|
|
|
//
|
|
|
|
// type JoinStruct struct {
|
2016-08-23 05:14:46 +02:00
|
|
|
// // User1 can have it's struct fields bound to since it specifies
|
|
|
|
// // ,bind in the struct tag, it will look specifically for
|
|
|
|
// // fields that are prefixed with "user." returning from the query.
|
|
|
|
// // For example "user.id" column name will bind to User1.ID
|
2016-08-08 02:11:45 +02:00
|
|
|
// User1 *models.User `boil:"user,bind"`
|
2016-08-23 05:14:46 +02:00
|
|
|
// // User2 will follow the same rules as noted above except it will use
|
|
|
|
// // "friend." as the prefix it's looking for.
|
2016-08-08 02:11:45 +02:00
|
|
|
// User2 *models.User `boil:"friend,bind"`
|
|
|
|
// // RandomData will not be recursed into to look for fields to
|
|
|
|
// // bind and will not be bound to because of the - for the name.
|
|
|
|
// RandomData myStruct `boil:"-"`
|
|
|
|
// // Date will not be recursed into to look for fields to bind because
|
|
|
|
// // it does not specify ,bind in the struct tag. But it can be bound to
|
|
|
|
// // as it does not specify a - for the name.
|
|
|
|
// Date time.Time
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// models.Users(qm.InnerJoin("users as friend on users.friend_id = friend.id")).Bind(&joinStruct)
|
2016-08-18 03:52:42 +02:00
|
|
|
//
|
|
|
|
// For custom objects that want to use eager loading, please see the
|
|
|
|
// loadRelationships function.
|
2016-08-12 07:57:07 +02:00
|
|
|
func Bind(rows *sql.Rows, obj interface{}) error {
|
|
|
|
structType, sliceType, singular, err := bindChecks(obj)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-09-02 09:09:42 +02:00
|
|
|
return bind(rows, obj, structType, sliceType, singular)
|
2016-08-12 07:57:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Bind executes the query and inserts the
|
|
|
|
// result into the passed in object pointer
|
|
|
|
//
|
|
|
|
// See documentation for boil.Bind()
|
2016-05-17 12:00:56 +02:00
|
|
|
func (q *Query) Bind(obj interface{}) error {
|
2016-09-03 19:33:28 +02:00
|
|
|
structType, sliceType, bkind, err := bindChecks(obj)
|
2016-08-12 07:57:07 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-09-14 18:14:30 +02:00
|
|
|
rows, err := q.Query()
|
2016-08-12 07:57:07 +02:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "bind failed to execute query")
|
|
|
|
}
|
|
|
|
defer rows.Close()
|
2016-09-03 19:33:28 +02:00
|
|
|
if res := bind(rows, obj, structType, sliceType, bkind); res != nil {
|
2016-08-17 19:56:00 +02:00
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2016-09-03 19:33:28 +02:00
|
|
|
if len(q.load) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-09-03 08:53:37 +02:00
|
|
|
state := loadRelationshipState{
|
|
|
|
exec: q.executor,
|
|
|
|
loaded: map[string]struct{}{},
|
2016-08-17 19:56:00 +02:00
|
|
|
}
|
2016-09-03 08:53:37 +02:00
|
|
|
for _, toLoad := range q.load {
|
|
|
|
state.toLoad = strings.Split(toLoad, ".")
|
2016-09-03 19:33:28 +02:00
|
|
|
if err = state.loadRelationships(0, obj, bkind); err != nil {
|
2016-08-30 07:21:32 +02:00
|
|
|
return err
|
2016-08-17 19:56:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
2016-08-12 07:57:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// bindChecks resolves information about the bind target, and errors if it's not an object
|
|
|
|
// we can bind to.
|
2016-09-03 19:33:28 +02:00
|
|
|
func bindChecks(obj interface{}) (structType reflect.Type, sliceType reflect.Type, bkind bindKind, err error) {
|
2016-08-08 02:11:45 +02:00
|
|
|
typ := reflect.TypeOf(obj)
|
2016-06-08 07:45:34 +02:00
|
|
|
kind := typ.Kind()
|
|
|
|
|
2016-09-03 19:33:28 +02:00
|
|
|
setErr := func() {
|
|
|
|
err = errors.Errorf("obj type should be *Type, *[]Type, or *[]*Type but was %q", reflect.TypeOf(obj).String())
|
|
|
|
}
|
2016-08-08 02:11:45 +02:00
|
|
|
|
2016-09-03 19:33:28 +02:00
|
|
|
for i := 0; ; i++ {
|
|
|
|
switch i {
|
|
|
|
case 0:
|
|
|
|
if kind != reflect.Ptr {
|
|
|
|
setErr()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
case 1:
|
|
|
|
switch kind {
|
|
|
|
case reflect.Struct:
|
2016-08-08 02:11:45 +02:00
|
|
|
structType = typ
|
2016-09-03 19:33:28 +02:00
|
|
|
bkind = kindStruct
|
|
|
|
return
|
|
|
|
case reflect.Slice:
|
|
|
|
sliceType = typ
|
|
|
|
default:
|
|
|
|
setErr()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
case 2:
|
|
|
|
switch kind {
|
|
|
|
case reflect.Struct:
|
|
|
|
structType = typ
|
|
|
|
bkind = kindSliceStruct
|
|
|
|
return
|
|
|
|
case reflect.Ptr:
|
|
|
|
default:
|
|
|
|
setErr()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
case 3:
|
|
|
|
if kind != reflect.Struct {
|
|
|
|
setErr()
|
|
|
|
return
|
2016-08-08 02:11:45 +02:00
|
|
|
}
|
|
|
|
structType = typ
|
2016-09-03 19:33:28 +02:00
|
|
|
bkind = kindPtrSliceStruct
|
|
|
|
return
|
2016-06-08 07:45:34 +02:00
|
|
|
}
|
|
|
|
|
2016-09-03 19:33:28 +02:00
|
|
|
typ = typ.Elem()
|
|
|
|
kind = typ.Kind()
|
|
|
|
}
|
2016-05-17 12:00:56 +02:00
|
|
|
}
|
|
|
|
|
2016-09-03 19:33:28 +02:00
|
|
|
func bind(rows *sql.Rows, obj interface{}, structType, sliceType reflect.Type, bkind bindKind) error {
|
2016-08-08 02:11:45 +02:00
|
|
|
cols, err := rows.Columns()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "bind failed to get column names")
|
2016-06-05 08:13:38 +02:00
|
|
|
}
|
|
|
|
|
2016-08-08 02:11:45 +02:00
|
|
|
var ptrSlice reflect.Value
|
2016-09-03 19:33:28 +02:00
|
|
|
switch bkind {
|
2016-09-03 19:43:32 +02:00
|
|
|
case kindSliceStruct, kindPtrSliceStruct:
|
2016-08-08 03:06:09 +02:00
|
|
|
ptrSlice = reflect.Indirect(reflect.ValueOf(obj))
|
2016-08-08 02:11:45 +02:00
|
|
|
}
|
|
|
|
|
2016-09-03 20:37:56 +02:00
|
|
|
var strMapping map[string]uint64
|
|
|
|
var sok bool
|
2016-08-31 09:09:13 +02:00
|
|
|
var mapping []uint64
|
2016-08-31 09:47:35 +02:00
|
|
|
var ok bool
|
|
|
|
|
2016-09-03 20:37:56 +02:00
|
|
|
typStr := structType.String()
|
|
|
|
|
|
|
|
mapKey := makeCacheKey(typStr, cols)
|
2016-08-31 09:47:35 +02:00
|
|
|
mut.RLock()
|
2016-09-01 06:18:30 +02:00
|
|
|
mapping, ok = bindingMaps[mapKey]
|
2016-09-03 20:37:56 +02:00
|
|
|
if !ok {
|
|
|
|
if strMapping, sok = structMaps[typStr]; !sok {
|
|
|
|
strMapping = MakeStructMapping(structType)
|
|
|
|
}
|
|
|
|
}
|
2016-08-31 09:47:35 +02:00
|
|
|
mut.RUnlock()
|
|
|
|
|
|
|
|
if !ok {
|
2016-09-03 20:37:56 +02:00
|
|
|
mapping, err = BindMapping(structType, strMapping, cols)
|
2016-08-31 09:47:35 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
mut.Lock()
|
2016-09-03 20:37:56 +02:00
|
|
|
if !sok {
|
|
|
|
structMaps[typStr] = strMapping
|
|
|
|
}
|
2016-09-01 06:18:30 +02:00
|
|
|
bindingMaps[mapKey] = mapping
|
2016-08-31 09:47:35 +02:00
|
|
|
mut.Unlock()
|
2016-08-31 09:09:13 +02:00
|
|
|
}
|
|
|
|
|
2016-09-03 19:43:32 +02:00
|
|
|
var oneStruct reflect.Value
|
|
|
|
if bkind == kindSliceStruct {
|
|
|
|
oneStruct = reflect.Indirect(reflect.New(structType))
|
|
|
|
}
|
|
|
|
|
2016-08-13 20:42:28 +02:00
|
|
|
foundOne := false
|
2016-08-08 02:11:45 +02:00
|
|
|
for rows.Next() {
|
2016-08-13 20:42:28 +02:00
|
|
|
foundOne = true
|
2016-08-08 02:11:45 +02:00
|
|
|
var newStruct reflect.Value
|
|
|
|
var pointers []interface{}
|
|
|
|
|
2016-09-03 19:33:28 +02:00
|
|
|
switch bkind {
|
|
|
|
case kindStruct:
|
2016-09-03 21:47:27 +02:00
|
|
|
pointers = PtrsFromMapping(reflect.Indirect(reflect.ValueOf(obj)), mapping)
|
2016-09-03 19:43:32 +02:00
|
|
|
case kindSliceStruct:
|
2016-09-03 21:47:27 +02:00
|
|
|
pointers = PtrsFromMapping(oneStruct, mapping)
|
2016-09-03 19:33:28 +02:00
|
|
|
case kindPtrSliceStruct:
|
2016-08-08 02:11:45 +02:00
|
|
|
newStruct = reflect.New(structType)
|
2016-09-03 21:47:27 +02:00
|
|
|
pointers = PtrsFromMapping(reflect.Indirect(newStruct), mapping)
|
2016-08-08 02:11:45 +02:00
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := rows.Scan(pointers...); err != nil {
|
|
|
|
return errors.Wrap(err, "failed to bind pointers to obj")
|
|
|
|
}
|
|
|
|
|
2016-09-03 19:33:28 +02:00
|
|
|
switch bkind {
|
2016-09-03 19:43:32 +02:00
|
|
|
case kindSliceStruct:
|
|
|
|
ptrSlice.Set(reflect.Append(ptrSlice, oneStruct))
|
2016-09-03 19:33:28 +02:00
|
|
|
case kindPtrSliceStruct:
|
2016-08-08 02:11:45 +02:00
|
|
|
ptrSlice.Set(reflect.Append(ptrSlice, newStruct))
|
|
|
|
}
|
2016-06-05 08:13:38 +02:00
|
|
|
}
|
|
|
|
|
2016-09-03 19:33:28 +02:00
|
|
|
if bkind == kindStruct && !foundOne {
|
2016-08-13 20:42:28 +02:00
|
|
|
return sql.ErrNoRows
|
|
|
|
}
|
|
|
|
|
2016-05-17 12:00:56 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-09-03 20:37:56 +02:00
|
|
|
// BindMapping creates a mapping that helps look up the pointer for the
|
2016-09-03 08:53:37 +02:00
|
|
|
// column given.
|
2016-09-03 20:37:56 +02:00
|
|
|
func BindMapping(typ reflect.Type, mapping map[string]uint64, cols []string) ([]uint64, error) {
|
2016-08-31 09:09:13 +02:00
|
|
|
ptrs := make([]uint64, len(cols))
|
2016-06-05 08:13:38 +02:00
|
|
|
|
2016-08-31 09:09:13 +02:00
|
|
|
ColLoop:
|
2016-08-08 02:11:45 +02:00
|
|
|
for i, c := range cols {
|
2016-09-02 09:09:42 +02:00
|
|
|
name := strmangle.TitleCaseIdentifier(c)
|
2016-08-31 09:09:13 +02:00
|
|
|
ptrMap, ok := mapping[name]
|
|
|
|
if ok {
|
|
|
|
ptrs[i] = ptrMap
|
|
|
|
continue
|
2016-06-07 06:38:17 +02:00
|
|
|
}
|
2016-06-05 08:13:38 +02:00
|
|
|
|
2016-08-31 09:09:13 +02:00
|
|
|
suffix := "." + name
|
|
|
|
for maybeMatch, mapping := range mapping {
|
|
|
|
if strings.HasSuffix(maybeMatch, suffix) {
|
|
|
|
ptrs[i] = mapping
|
|
|
|
continue ColLoop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, errors.Errorf("could not find struct field name in mapping: %s", name)
|
2016-08-08 02:11:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return ptrs, nil
|
|
|
|
}
|
|
|
|
|
2016-09-03 21:47:27 +02:00
|
|
|
// PtrsFromMapping expects to be passed an addressable struct and a mapping
|
|
|
|
// of where to find things. It pulls the pointers out referred to by the mapping.
|
|
|
|
func PtrsFromMapping(val reflect.Value, mapping []uint64) []interface{} {
|
|
|
|
ptrs := make([]interface{}, len(mapping))
|
|
|
|
for i, m := range mapping {
|
|
|
|
ptrs[i] = ptrFromMapping(val, m, true).Interface()
|
|
|
|
}
|
|
|
|
return ptrs
|
|
|
|
}
|
|
|
|
|
|
|
|
// ValuesFromMapping expects to be passed an addressable struct and a mapping
|
|
|
|
// of where to find things. It pulls the pointers out referred to by the mapping.
|
|
|
|
func ValuesFromMapping(val reflect.Value, mapping []uint64) []interface{} {
|
2016-08-31 09:09:13 +02:00
|
|
|
ptrs := make([]interface{}, len(mapping))
|
|
|
|
for i, m := range mapping {
|
2016-09-03 21:47:27 +02:00
|
|
|
ptrs[i] = ptrFromMapping(val, m, false).Interface()
|
2016-08-31 09:09:13 +02:00
|
|
|
}
|
|
|
|
return ptrs
|
|
|
|
}
|
|
|
|
|
|
|
|
// ptrFromMapping expects to be passed an addressable struct that it's looking
|
|
|
|
// for things on.
|
2016-09-03 21:47:27 +02:00
|
|
|
func ptrFromMapping(val reflect.Value, mapping uint64, addressOf bool) reflect.Value {
|
2016-08-31 09:09:13 +02:00
|
|
|
for i := 0; i < 8; i++ {
|
|
|
|
v := (mapping >> uint(i*8)) & sentinel
|
|
|
|
|
|
|
|
if v == sentinel {
|
2016-09-05 17:24:19 +02:00
|
|
|
if addressOf && val.Kind() != reflect.Ptr {
|
2016-08-31 09:09:13 +02:00
|
|
|
return val.Addr()
|
2016-09-05 17:24:19 +02:00
|
|
|
} else if !addressOf && val.Kind() == reflect.Ptr {
|
|
|
|
return reflect.Indirect(val)
|
2016-08-31 09:09:13 +02:00
|
|
|
}
|
|
|
|
return val
|
|
|
|
}
|
|
|
|
|
|
|
|
val = val.Field(int(v))
|
|
|
|
if val.Kind() == reflect.Ptr {
|
|
|
|
val = reflect.Indirect(val)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
panic("could not find pointer from mapping")
|
|
|
|
}
|
|
|
|
|
2016-09-03 20:37:56 +02:00
|
|
|
// MakeStructMapping creates a map of the struct to be able to quickly look
|
|
|
|
// up its pointers and values by name.
|
|
|
|
func MakeStructMapping(typ reflect.Type) map[string]uint64 {
|
2016-08-31 09:09:13 +02:00
|
|
|
fieldMaps := make(map[string]uint64)
|
2016-09-02 09:09:42 +02:00
|
|
|
makeStructMappingHelper(typ, "", 0, 0, fieldMaps)
|
2016-08-31 09:09:13 +02:00
|
|
|
return fieldMaps
|
|
|
|
}
|
|
|
|
|
2016-09-02 09:09:42 +02:00
|
|
|
func makeStructMappingHelper(typ reflect.Type, prefix string, current uint64, depth uint, fieldMaps map[string]uint64) {
|
2016-08-31 09:09:13 +02:00
|
|
|
if typ.Kind() == reflect.Ptr {
|
|
|
|
typ = typ.Elem()
|
|
|
|
}
|
|
|
|
|
|
|
|
n := typ.NumField()
|
|
|
|
for i := 0; i < n; i++ {
|
|
|
|
f := typ.Field(i)
|
|
|
|
|
2016-09-02 09:09:42 +02:00
|
|
|
tag, recurse := getBoilTag(f)
|
2016-08-31 09:09:13 +02:00
|
|
|
if len(tag) == 0 {
|
|
|
|
tag = f.Name
|
|
|
|
} else if tag[0] == '-' {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(prefix) != 0 {
|
|
|
|
tag = fmt.Sprintf("%s.%s", prefix, tag)
|
|
|
|
}
|
|
|
|
|
|
|
|
if recurse {
|
2016-09-02 09:09:42 +02:00
|
|
|
makeStructMappingHelper(f.Type, tag, current|uint64(i)<<depth, depth+8, fieldMaps)
|
2016-08-31 09:09:13 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
fieldMaps[tag] = current | (sentinel << (depth + 8)) | (uint64(i) << depth)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-02 09:09:42 +02:00
|
|
|
func getBoilTag(field reflect.StructField) (name string, recurse bool) {
|
2016-08-08 02:11:45 +02:00
|
|
|
tag := field.Tag.Get("boil")
|
2016-08-26 04:26:58 +02:00
|
|
|
name = field.Name
|
2016-08-08 02:11:45 +02:00
|
|
|
|
2016-08-26 04:26:58 +02:00
|
|
|
if len(tag) == 0 {
|
|
|
|
return name, false
|
|
|
|
}
|
|
|
|
|
|
|
|
ind := strings.IndexByte(tag, ',')
|
|
|
|
if ind == -1 {
|
2016-09-02 09:09:42 +02:00
|
|
|
return strmangle.TitleCase(tag), false
|
2016-08-26 04:26:58 +02:00
|
|
|
} else if ind == 0 {
|
|
|
|
return name, true
|
2016-08-08 03:06:09 +02:00
|
|
|
}
|
|
|
|
|
2016-08-26 04:26:58 +02:00
|
|
|
nameFragment := tag[:ind]
|
2016-09-02 09:09:42 +02:00
|
|
|
return strmangle.TitleCase(nameFragment), true
|
2016-05-17 12:00:56 +02:00
|
|
|
}
|
|
|
|
|
2016-09-01 06:18:30 +02:00
|
|
|
func makeCacheKey(typ string, cols []string) string {
|
|
|
|
buf := strmangle.GetBuffer()
|
|
|
|
buf.WriteString(typ)
|
|
|
|
for _, s := range cols {
|
|
|
|
buf.WriteString(s)
|
|
|
|
}
|
|
|
|
mapKey := buf.String()
|
|
|
|
strmangle.PutBuffer(buf)
|
|
|
|
|
|
|
|
return mapKey
|
|
|
|
}
|