Add TitleCase cache

This commit is contained in:
Patrick O'brien 2016-09-02 17:09:42 +10:00
parent b101df0a24
commit 09fb8005f6
13 changed files with 69 additions and 103 deletions

View file

@ -20,7 +20,7 @@ same SQL helpers and wrappers for every project we were creating, but did not wa
there that utilize the "code-first" approach. `SQLX` is a great project, but very minimalistic and still requires a
considerable amount of boilerplate for every project. Originally this project started as a SQL boilerplate generator (hence the name)
that generated simple helper functions, but we found that we could accomplish the same task by turning it into a
(mostly) fully fledged ORM, without any sacrifice in performance or congruency, but generous gains in flexibility.
(mostly) fully fledged ORM generator, without any sacrifice in performance or congruency, but generous gains in flexibility.
The approach we've taken has afforded us the following benefits:

View file

@ -74,18 +74,12 @@ func (q *Query) BindP(obj interface{}) {
// For custom objects that want to use eager loading, please see the
// loadRelationships function.
func Bind(rows *sql.Rows, obj interface{}) error {
return BindFast(rows, obj, nil)
}
// BindFast uses a lookup table for column_name to ColumnName to avoid TitleCase.
func BindFast(rows *sql.Rows, obj interface{}, titleCases map[string]string) error {
structType, sliceType, singular, err := bindChecks(obj)
if err != nil {
return err
}
return bind(rows, obj, structType, sliceType, singular, titleCases)
return bind(rows, obj, structType, sliceType, singular)
}
// Bind executes the query and inserts the
@ -93,11 +87,6 @@ func BindFast(rows *sql.Rows, obj interface{}, titleCases map[string]string) err
//
// See documentation for boil.Bind()
func (q *Query) Bind(obj interface{}) error {
return q.BindFast(obj, nil)
}
// BindFast uses a lookup table for column_name to ColumnName to avoid TitleCase.
func (q *Query) BindFast(obj interface{}, titleCases map[string]string) error {
structType, sliceType, singular, err := bindChecks(obj)
if err != nil {
return err
@ -108,8 +97,7 @@ func (q *Query) BindFast(obj interface{}, titleCases map[string]string) error {
return errors.Wrap(err, "bind failed to execute query")
}
defer rows.Close()
if res := bind(rows, obj, structType, sliceType, singular, titleCases); res != nil {
if res := bind(rows, obj, structType, sliceType, singular); res != nil {
return res
}
@ -255,7 +243,7 @@ func bindChecks(obj interface{}) (structType reflect.Type, sliceType reflect.Typ
return structType, sliceType, singular, nil
}
func bind(rows *sql.Rows, obj interface{}, structType, sliceType reflect.Type, singular bool, titleCases map[string]string) error {
func bind(rows *sql.Rows, obj interface{}, structType, sliceType reflect.Type, singular bool) error {
cols, err := rows.Columns()
if err != nil {
return errors.Wrap(err, "bind failed to get column names")
@ -275,7 +263,7 @@ func bind(rows *sql.Rows, obj interface{}, structType, sliceType reflect.Type, s
mut.RUnlock()
if !ok {
mapping, err = bindMapping(structType, titleCases, cols)
mapping, err = bindMapping(structType, cols)
if err != nil {
return err
}
@ -317,13 +305,13 @@ func bind(rows *sql.Rows, obj interface{}, structType, sliceType reflect.Type, s
return nil
}
func bindMapping(typ reflect.Type, titleCases map[string]string, cols []string) ([]uint64, error) {
func bindMapping(typ reflect.Type, cols []string) ([]uint64, error) {
ptrs := make([]uint64, len(cols))
mapping := makeStructMapping(typ, titleCases)
mapping := makeStructMapping(typ)
ColLoop:
for i, c := range cols {
name := strmangle.TitleCaseIdentifier(c, titleCases)
name := strmangle.TitleCaseIdentifier(c)
ptrMap, ok := mapping[name]
if ok {
ptrs[i] = ptrMap
@ -376,13 +364,13 @@ func ptrFromMapping(val reflect.Value, mapping uint64) reflect.Value {
panic("could not find pointer from mapping")
}
func makeStructMapping(typ reflect.Type, titleCases map[string]string) map[string]uint64 {
func makeStructMapping(typ reflect.Type) map[string]uint64 {
fieldMaps := make(map[string]uint64)
makeStructMappingHelper(typ, "", 0, 0, fieldMaps, titleCases)
makeStructMappingHelper(typ, "", 0, 0, fieldMaps)
return fieldMaps
}
func makeStructMappingHelper(typ reflect.Type, prefix string, current uint64, depth uint, fieldMaps map[string]uint64, titleCases map[string]string) {
func makeStructMappingHelper(typ reflect.Type, prefix string, current uint64, depth uint, fieldMaps map[string]uint64) {
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
@ -391,7 +379,7 @@ func makeStructMappingHelper(typ reflect.Type, prefix string, current uint64, de
for i := 0; i < n; i++ {
f := typ.Field(i)
tag, recurse := getBoilTag(f, titleCases)
tag, recurse := getBoilTag(f)
if len(tag) == 0 {
tag = f.Name
} else if tag[0] == '-' {
@ -403,7 +391,7 @@ func makeStructMappingHelper(typ reflect.Type, prefix string, current uint64, de
}
if recurse {
makeStructMappingHelper(f.Type, tag, current|uint64(i)<<depth, depth+8, fieldMaps, titleCases)
makeStructMappingHelper(f.Type, tag, current|uint64(i)<<depth, depth+8, fieldMaps)
continue
}
@ -411,7 +399,7 @@ func makeStructMappingHelper(typ reflect.Type, prefix string, current uint64, de
}
}
func getBoilTag(field reflect.StructField, titleCases map[string]string) (name string, recurse bool) {
func getBoilTag(field reflect.StructField) (name string, recurse bool) {
tag := field.Tag.Get("boil")
name = field.Name
@ -419,24 +407,15 @@ func getBoilTag(field reflect.StructField, titleCases map[string]string) (name s
return name, false
}
var ok bool
ind := strings.IndexByte(tag, ',')
if ind == -1 {
name, ok = titleCases[tag]
if !ok {
name = strmangle.TitleCase(tag)
}
return name, false
return strmangle.TitleCase(tag), false
} else if ind == 0 {
return name, true
}
nameFragment := tag[:ind]
name, ok = titleCases[nameFragment]
if !ok {
name = strmangle.TitleCase(nameFragment)
}
return name, true
return strmangle.TitleCase(nameFragment), true
}
func makeCacheKey(typ string, cols []string) string {
@ -452,17 +431,12 @@ func makeCacheKey(typ string, cols []string) string {
}
// GetStructValues returns the values (as interface) of the matching columns in obj
func GetStructValues(obj interface{}, titleCases map[string]string, columns ...string) []interface{} {
func GetStructValues(obj interface{}, columns ...string) []interface{} {
ret := make([]interface{}, len(columns))
val := reflect.Indirect(reflect.ValueOf(obj))
for i, c := range columns {
var fieldName string
if titleCases == nil {
fieldName = strmangle.TitleCase(c)
} else {
fieldName = titleCases[c]
}
fieldName := strmangle.TitleCase(c)
field := val.FieldByName(fieldName)
if !field.IsValid() {
panic(fmt.Sprintf("unable to find field with name: %s\n%#v", fieldName, obj))
@ -474,19 +448,13 @@ func GetStructValues(obj interface{}, titleCases map[string]string, columns ...s
}
// GetSliceValues returns the values (as interface) of the matching columns in obj.
func GetSliceValues(slice []interface{}, titleCases map[string]string, columns ...string) []interface{} {
func GetSliceValues(slice []interface{}, columns ...string) []interface{} {
ret := make([]interface{}, len(slice)*len(columns))
for i, obj := range slice {
val := reflect.Indirect(reflect.ValueOf(obj))
for j, c := range columns {
var fieldName string
if titleCases == nil {
fieldName = strmangle.TitleCase(c)
} else {
fieldName = titleCases[c]
}
fieldName := strmangle.TitleCase(c)
field := val.FieldByName(fieldName)
if !field.IsValid() {
panic(fmt.Sprintf("unable to find field with name: %s\n%#v", fieldName, obj))
@ -499,7 +467,7 @@ func GetSliceValues(slice []interface{}, titleCases map[string]string, columns .
}
// GetStructPointers returns a slice of pointers to the matching columns in obj
func GetStructPointers(obj interface{}, titleCases map[string]string, columns ...string) []interface{} {
func GetStructPointers(obj interface{}, columns ...string) []interface{} {
val := reflect.ValueOf(obj).Elem()
var ln int
@ -513,14 +481,7 @@ func GetStructPointers(obj interface{}, titleCases map[string]string, columns ..
} else {
ln = len(columns)
getField = func(v reflect.Value, i int) reflect.Value {
var fieldName string
if titleCases == nil {
fieldName = strmangle.TitleCase(columns[i])
} else {
fieldName = titleCases[columns[i]]
}
return v.FieldByName(fieldName)
return v.FieldByName(strmangle.TitleCase(columns[i]))
}
}

View file

@ -115,7 +115,7 @@ func TestMakeStructMapping(t *testing.T) {
} `boil:",bind"`
}{}
got := makeStructMapping(reflect.TypeOf(testStruct), nil)
got := makeStructMapping(reflect.TypeOf(testStruct))
expectMap := map[string]uint64{
"Different": testMakeMapping(0),
@ -189,16 +189,6 @@ func TestGetBoilTag(t *testing.T) {
Nose string
}
var testTitleCases = map[string]string{
"test_one": "TestOne",
"test_two": "TestTwo",
"middle_name": "MiddleName",
"awesome_name": "AwesomeName",
"age": "Age",
"face": "Face",
"nose": "Nose",
}
var structFields []reflect.StructField
typ := reflect.TypeOf(TestStruct{})
removeOk := func(thing reflect.StructField, ok bool) reflect.StructField {
@ -228,7 +218,7 @@ func TestGetBoilTag(t *testing.T) {
{"Nose", false},
}
for i, s := range structFields {
name, recurse := getBoilTag(s, testTitleCases)
name, recurse := getBoilTag(s)
if expect[i].Name != name {
t.Errorf("Invalid name, expect %q, got %q", expect[i].Name, name)
}
@ -665,7 +655,7 @@ func TestGetStructValues(t *testing.T) {
NullBool: null.NewBool(true, false),
}
vals := GetStructValues(&o, nil, "title_thing", "name", "id", "stuff", "things", "time", "null_bool")
vals := GetStructValues(&o, "title_thing", "name", "id", "stuff", "things", "time", "null_bool")
if vals[0].(string) != "patrick" {
t.Errorf("Want test, got %s", vals[0])
}
@ -704,7 +694,7 @@ func TestGetSliceValues(t *testing.T) {
in[0] = o[0]
in[1] = o[1]
vals := GetSliceValues(in, nil, "id", "name")
vals := GetSliceValues(in, "id", "name")
if got := vals[0].(int); got != 5 {
t.Error(got)
}
@ -729,7 +719,7 @@ func TestGetStructPointers(t *testing.T) {
Title: "patrick",
}
ptrs := GetStructPointers(&o, nil, "title", "id")
ptrs := GetStructPointers(&o, "title", "id")
*ptrs[0].(*string) = "test"
if o.Title != "test" {
t.Errorf("Expected test, got %s", o.Title)

View file

@ -9,6 +9,7 @@ import (
"math"
"regexp"
"strings"
"sync"
)
var (
@ -150,6 +151,13 @@ func Singular(name string) string {
return buf.String()
}
// titleCaseCache holds the mapping of title cases.
// Example: map["MyWord"] == "my_word"
var (
mut sync.RWMutex
titleCaseCache = map[string]string{}
)
// TitleCase changes a snake-case variable name
// into a go styled object variable name of "ColumnName".
// titleCase also fully uppercases "ID" components of names, for example
@ -158,6 +166,14 @@ func Singular(name string) string {
// Note: This method is ugly because it has been highly optimized,
// we found that it was a fairly large bottleneck when we were using regexp.
func TitleCase(n string) string {
// Attempt to fetch from cache
mut.RLock()
val, ok := titleCaseCache[n]
mut.RUnlock()
if ok {
return val
}
ln := len(n)
name := []byte(n)
buf := GetBuffer()
@ -219,6 +235,12 @@ func TitleCase(n string) string {
ret := buf.String()
PutBuffer(buf)
// Cache the title case result
mut.Lock()
titleCaseCache[n] = ret
mut.Unlock()
return ret
}
@ -264,14 +286,10 @@ func CamelCase(name string) string {
// TitleCaseIdentifier splits on dots and then titlecases each fragment.
// map titleCase (split c ".")
func TitleCaseIdentifier(id string, titleCases map[string]string) string {
func TitleCaseIdentifier(id string) string {
nextDot := strings.IndexByte(id, '.')
if nextDot < 0 {
titled, ok := titleCases[id]
if !ok {
titled = TitleCase(id)
}
return titled
return TitleCase(id)
}
buf := GetBuffer()
@ -283,10 +301,7 @@ func TitleCaseIdentifier(id string, titleCases map[string]string) string {
fmt.Println(lastDot, nextDot)
fragment := id[lastDot:nextDot]
titled, ok := titleCases[fragment]
if !ok {
titled = TitleCase(fragment)
}
titled := TitleCase(fragment)
if addDots {
buf.WriteByte('.')

View file

@ -242,7 +242,7 @@ func TestTitleCaseIdentifier(t *testing.T) {
}
for i, test := range tests {
if out := TitleCaseIdentifier(test.In, nil); out != test.Out {
if out := TitleCaseIdentifier(test.In); out != test.Out {
t.Errorf("[%d] (%s) Out was wrong: %q, want: %q", i, test.In, out, test.Out)
}
}

View file

@ -16,7 +16,7 @@ func (q {{$varNameSingular}}Query) One() (*{{$tableNameSingular}}, error) {
boil.SetLimit(q.Query, 1)
err := q.BindFast(o, {{$varNameSingular}}TitleCases)
err := q.Bind(o)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, sql.ErrNoRows
@ -47,7 +47,7 @@ func (q {{$varNameSingular}}Query) AllP() {{$tableNameSingular}}Slice {
func (q {{$varNameSingular}}Query) All() ({{$tableNameSingular}}Slice, error) {
var o {{$tableNameSingular}}Slice
err := q.BindFast(&o, {{$varNameSingular}}TitleCases)
err := q.Bind(&o)
if err != nil {
return nil, errors.Wrap(err, "{{.PkgName}}: failed to assign all query results to {{$tableNameSingular}} slice")
}

View file

@ -43,7 +43,7 @@ func ({{$varNameSingular}}L) Load{{.Function.Name}}(e boil.Executor, singular bo
defer results.Close()
var resultSlice []*{{.ForeignTable.NameGo}}
if err = boil.BindFast(results, &resultSlice, {{.ForeignTable.Name | singular | camelCase}}TitleCases); err != nil {
if err = boil.Bind(results, &resultSlice); err != nil {
return errors.Wrap(err, "failed to bind eager loaded slice {{.ForeignTable.NameGo}}")
}

View file

@ -78,7 +78,7 @@ func ({{$varNameSingular}}L) Load{{$txt.Function.Name}}(e boil.Executor, singula
return errors.Wrap(err, "failed to plebian-bind eager loaded slice {{.ForeignTable}}")
}
{{else -}}
if err = boil.BindFast(results, &resultSlice, {{.ForeignTable | singular | camelCase}}TitleCases); err != nil {
if err = boil.Bind(results, &resultSlice); err != nil {
return errors.Wrap(err, "failed to bind eager loaded slice {{.ForeignTable}}")
}
{{end}}

View file

@ -34,7 +34,7 @@ func Find{{$tableNameSingular}}(exec boil.Executor, {{$pkArgs}}, selectCols ...s
q := boil.SQL(exec, query, {{$pkNames | join ", "}})
err := q.BindFast({{$varNameSingular}}Obj, {{$varNameSingular}}TitleCases)
err := q.Bind({{$varNameSingular}}Obj)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, sql.ErrNoRows

View file

@ -53,10 +53,10 @@ func (o *{{$tableNameSingular}}) Insert(exec boil.Executor, whitelist ... string
{{if .UseLastInsertID}}
if boil.DebugMode {
fmt.Fprintln(boil.DebugWriter, ins)
fmt.Fprintln(boil.DebugWriter, boil.GetStructValues(o, {{$varNameSingular}}TitleCases, wl...))
fmt.Fprintln(boil.DebugWriter, boil.GetStructValues(o, wl...))
}
result, err := exec.Exec(ins, boil.GetStructValues(o, {{$varNameSingular}}TitleCases, wl...)...)
result, err := exec.Exec(ins, boil.GetStructValues(o, wl...)...)
if err != nil {
return errors.Wrap(err, "{{.PkgName}}: unable to insert into {{.Table.Name}}")
}
@ -77,21 +77,21 @@ func (o *{{$tableNameSingular}}) Insert(exec boil.Executor, whitelist ... string
}
sel := fmt.Sprintf(`SELECT %s FROM {{.Table.Name}} WHERE %s`, strings.Join(returnColumns, `","`), strmangle.WhereClause(1, {{$varNameSingular}}AutoIncPrimaryKeys))
err = exec.QueryRow(sel, lastID).Scan(boil.GetStructPointers(o, {{$varNameSingular}}TitleCases, returnColumns...))
err = exec.QueryRow(sel, lastID).Scan(boil.GetStructPointers(o, returnColumns...))
if err != nil {
return errors.Wrap(err, "{{.PkgName}}: unable to populate default values for {{.Table.Name}}")
}
{{else}}
if len(returnColumns) != 0 {
ins = ins + fmt.Sprintf(` RETURNING %s`, strings.Join(returnColumns, ","))
err = exec.QueryRow(ins, boil.GetStructValues(o, {{$varNameSingular}}TitleCases, wl...)...).Scan(boil.GetStructPointers(o, {{$varNameSingular}}TitleCases, returnColumns...)...)
err = exec.QueryRow(ins, boil.GetStructValues(o, wl...)...).Scan(boil.GetStructPointers(o, returnColumns...)...)
} else {
_, err = exec.Exec(ins, boil.GetStructValues(o, {{$varNameSingular}}TitleCases, wl...)...)
_, err = exec.Exec(ins, boil.GetStructValues(o, wl...)...)
}
if boil.DebugMode {
fmt.Fprintln(boil.DebugWriter, ins)
fmt.Fprintln(boil.DebugWriter, boil.GetStructValues(o, {{$varNameSingular}}TitleCases, wl...))
fmt.Fprintln(boil.DebugWriter, boil.GetStructValues(o, wl...))
}
if err != nil {

View file

@ -53,7 +53,7 @@ func (o *{{$tableNameSingular}}) Update(exec boil.Executor, whitelist ... string
}
query = fmt.Sprintf(`UPDATE {{.Table.Name}} SET %s WHERE %s`, strmangle.SetParamNames(wl), strmangle.WhereClause(len(wl)+1, {{$varNameSingular}}PrimaryKeyColumns))
values = boil.GetStructValues(o, {{$varNameSingular}}TitleCases, wl...)
values = boil.GetStructValues(o, wl...)
values = append(values, {{.Table.PKey.Columns | stringMap .StringFuncs.titleCase | prefixStringSlice "o." | join ", "}})
if boil.DebugMode {

View file

@ -58,16 +58,16 @@ func (o *{{$tableNameSingular}}) Upsert(exec boil.Executor, updateOnConflict boo
if boil.DebugMode {
fmt.Fprintln(boil.DebugWriter, query)
fmt.Fprintln(boil.DebugWriter, boil.GetStructValues(o, {{$varNameSingular}}TitleCases, whitelist...))
fmt.Fprintln(boil.DebugWriter, boil.GetStructValues(o, whitelist...))
}
{{- if .UseLastInsertID}}
return errors.New("don't know how to do this yet")
{{- else}}
if len(ret) != 0 {
err = exec.QueryRow(query, boil.GetStructValues(o, {{$varNameSingular}}TitleCases, whitelist...)...).Scan(boil.GetStructPointers(o, {{$varNameSingular}}TitleCases, ret...)...)
err = exec.QueryRow(query, boil.GetStructValues(o, whitelist...)...).Scan(boil.GetStructPointers(o, ret...)...)
} else {
_, err = exec.Exec(query, boil.GetStructValues(o, {{$varNameSingular}}TitleCases, whitelist...)...)
_, err = exec.Exec(query, boil.GetStructValues(o, whitelist...)...)
}
{{- end}}

View file

@ -74,7 +74,7 @@ func (o *{{$tableNameSingular}}Slice) ReloadAll(exec boil.Executor) error {
q := boil.SQL(exec, sql, args...)
err := q.BindFast(&{{$varNamePlural}}, {{$varNameSingular}}TitleCases)
err := q.Bind(&{{$varNamePlural}})
if err != nil {
return errors.Wrap(err, "{{.PkgName}}: unable to reload all in {{$tableNameSingular}}Slice")
}