diff --git a/tpl/collections/collections_integration_test.go b/tpl/collections/collections_integration_test.go index ee003e049..750ea7cd8 100644 --- a/tpl/collections/collections_integration_test.go +++ b/tpl/collections/collections_integration_test.go @@ -588,6 +588,8 @@ All. seq := s.RegularPages() ns := s.TemplateStore.GetTemplateFuncsNamespace("collections").(*collections.Namespace) + b.ResetTimer() + b.Run("Where", func(b *testing.B) { for b.Loop() { v, err := ns.Where(context.Background(), seq, "Title", "ge", "Page0480") diff --git a/tpl/collections/sort.go b/tpl/collections/sort.go index 6d6a2ad56..15f6eb34a 100644 --- a/tpl/collections/sort.go +++ b/tpl/collections/sort.go @@ -89,7 +89,7 @@ func (ns *Namespace) Sort(ctx context.Context, l any, args ...any) (any, error) if !v.IsValid() { continue } - // Special handling of lower cased hmaps. + // Special handling of lower cased maps. if params, ok := v.Interface().(hmaps.Params); ok { v = reflect.ValueOf(params.GetNested(path[i+1:]...)) break diff --git a/tpl/collections/where.go b/tpl/collections/where.go index bc7dcd229..0cb84b8f5 100644 --- a/tpl/collections/where.go +++ b/tpl/collections/where.go @@ -393,16 +393,219 @@ func parseWhereArgs(args ...any) (mv reflect.Value, op string, err error) { return } +// elemResolver resolves a sub-element from a reflect.Value. +// Built once before the loop to avoid repeated type checks and reflect.ValueOf allocations. +type elemResolver func(reflect.Value) (reflect.Value, error) + +// newElemResolver returns a resolver optimized for the given element type and path. +// Returns nil if optimization isn't possible, in which case the caller should +// fall back to evaluateSubElem. +func (ns *Namespace) newElemResolver(ctxv reflect.Value, elemType reflect.Type, path []string) elemResolver { + if elemType.Kind() == reflect.Interface { + if len(path) != 1 { + return nil + } + return ns.newInterfaceMethodResolver(ctxv, elemType, path[0]) + } + + baseType := elemType + isPtr := baseType.Kind() == reflect.Pointer + if isPtr { + baseType = baseType.Elem() + } + + if baseType == reflect.TypeFor[hmaps.Params]() { + return func(v reflect.Value) (reflect.Value, error) { + if isPtr { + if v.IsNil() { + return zero, nil + } + v = v.Elem() + } + params := v.Interface().(hmaps.Params) + return reflect.ValueOf(params.GetNested(path...)), nil + } + } + + if len(path) != 1 { + return nil + } + name := path[0] + + // Check for method first, matching evaluateSubElem order. + ptrType := baseType + if !hreflect.IsInterfaceOrPointer(ptrType.Kind()) { + ptrType = reflect.PointerTo(baseType) + } + mt := hreflect.GetMethodByNameForType(ptrType, name) + if mt.Func.IsValid() { + return ns.newMethodResolver(ctxv, elemType, mt) + } + + switch baseType.Kind() { + case reflect.Map: + if baseType.Key().Kind() == reflect.String { + kv := reflect.ValueOf(name) + return func(v reflect.Value) (reflect.Value, error) { + if isPtr { + if v.IsNil() { + return zero, nil + } + v = v.Elem() + } + return v.MapIndex(kv), nil + } + } + case reflect.Struct: + ft, ok := baseType.FieldByName(name) + if ok { + if ft.PkgPath != "" && !ft.Anonymous { + return func(v reflect.Value) (reflect.Value, error) { + return zero, fmt.Errorf("%s is an unexported field of struct type %s", name, elemType) + } + } + idx := ft.Index + return func(v reflect.Value) (reflect.Value, error) { + if isPtr { + if v.IsNil() { + return zero, nil + } + v = v.Elem() + } + return v.FieldByIndex(idx), nil + } + } + } + + return nil +} + +func (ns *Namespace) newMethodResolver(ctxv reflect.Value, elemType reflect.Type, mt reflect.Method) elemResolver { + if mt.PkgPath != "" { + return func(v reflect.Value) (reflect.Value, error) { + return zero, fmt.Errorf("%s is an unexported method of type %s", mt.Name, elemType) + } + } + + numIn := mt.Type.NumIn() + maxNumIn := 1 + needsCtx := numIn > 1 && hreflect.IsContextType(mt.Type.In(1)) + if needsCtx { + maxNumIn = 2 + } + + switch { + case mt.Type.NumIn() > maxNumIn: + return nil + case mt.Type.NumOut() == 0: + return nil + case mt.Type.NumOut() > 2: + return nil + case mt.Type.NumOut() == 1 && mt.Type.Out(0).Implements(errorType): + return nil + case mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType): + return nil + } + + fn := mt.Func + hasErrOut := mt.Type.NumOut() == 2 + isPtr := elemType.Kind() == reflect.Pointer + + var callArgs []reflect.Value + if needsCtx { + callArgs = make([]reflect.Value, 2) + callArgs[1] = ctxv + } else { + callArgs = make([]reflect.Value, 1) + } + + return func(v reflect.Value) (reflect.Value, error) { + if isPtr && v.IsNil() { + return zero, nil + } + recv := v + if !isPtr && !hreflect.IsInterfaceOrPointer(recv.Kind()) && recv.CanAddr() { + recv = recv.Addr() + } + callArgs[0] = recv + res := fn.Call(callArgs) + if hasErrOut && !res[1].IsNil() { + return zero, res[1].Interface().(error) + } + return res[0], nil + } +} + +// newInterfaceMethodResolver returns a method resolver or nil if not possible, in which the caller should fall back to evaluateSubElem. +func (ns *Namespace) newInterfaceMethodResolver(ctxv reflect.Value, ifaceType reflect.Type, name string) elemResolver { + mt, ok := ifaceType.MethodByName(name) + if !ok { + return nil + } + + // For interface methods, Type does not include the receiver. + mType := mt.Type + numIn := mType.NumIn() + maxNumIn := 0 + needsCtx := numIn > 0 && hreflect.IsContextType(mType.In(0)) + if needsCtx { + maxNumIn = 1 + } + + switch { + case mType.NumIn() > maxNumIn: + return nil + case mType.NumOut() == 0: + return nil + case mType.NumOut() > 2: + return nil + case mType.NumOut() == 1 && mType.Out(0).Implements(errorType): + return nil + case mType.NumOut() == 2 && !mType.Out(1).Implements(errorType): + return nil + } + + index := mt.Index + hasErrOut := mType.NumOut() == 2 + + var callArgs []reflect.Value + if needsCtx { + callArgs = []reflect.Value{ctxv} + } + + return func(v reflect.Value) (reflect.Value, error) { + if v.IsNil() { + return zero, nil + } + res := v.Method(index).Call(callArgs) + if hasErrOut && !res[1].IsNil() { + return zero, res[1].Interface().(error) + } + return res[0], nil + } +} + // checkWhereArray handles the where-matching logic when the seqv value is an // Array or Slice. func (ns *Namespace) checkWhereArray(ctxv, seqv, kv, mv reflect.Value, path []string, op string) (any, error) { rv := reflect.MakeSlice(seqv.Type(), 0, 0) + var resolve elemResolver + if kv.Kind() == reflect.String && len(path) > 0 { + resolve = ns.newElemResolver(ctxv, seqv.Type().Elem(), path) + } + for i := range seqv.Len() { var vvv reflect.Value rvv := seqv.Index(i) - if kv.Kind() == reflect.String { + if resolve != nil { + var err error + vvv, err = resolve(rvv) + if err != nil { + return nil, err + } + } else if kv.Kind() == reflect.String { if params, ok := rvv.Interface().(hmaps.Params); ok { vvv = reflect.ValueOf(params.GetNested(path...)) } else { @@ -416,7 +619,6 @@ func (ns *Namespace) checkWhereArray(ctxv, seqv, kv, mv reflect.Value, path []st if i < len(path)-1 && vvv.IsValid() { if params, ok := vvv.Interface().(hmaps.Params); ok { - // The current path element is the map itself, .Params. vvv = reflect.ValueOf(params.GetNested(path[i+1:]...)) break } diff --git a/tpl/collections/where_test.go b/tpl/collections/where_test.go index 1822e9435..b4fd6e6a8 100644 --- a/tpl/collections/where_test.go +++ b/tpl/collections/where_test.go @@ -904,46 +904,6 @@ func BenchmarkWhereOps(b *testing.B) { }) } -func BenchmarkWhereMap(b *testing.B) { - ns := newNs() - seqString := map[string]string{} - seqAny := map[string]any{} - seqInt := map[string]int{} - - for i := range 1000 { - seqString[fmt.Sprintf("key%d", i)] = "value" - seqAny[fmt.Sprintf("key%d", i)] = "value" - seqInt[fmt.Sprintf("key%d", i)] = i - } - - b.Run("String", func(b *testing.B) { - for b.Loop() { - _, err := ns.Where(context.Background(), seqString, "key", "eq", "value") - if err != nil { - b.Fatal(err) - } - } - }) - - b.Run("Int", func(b *testing.B) { - for b.Loop() { - _, err := ns.Where(context.Background(), seqAny, "key", "eq", 42) - if err != nil { - b.Fatal(err) - } - } - }) - - b.Run("Any", func(b *testing.B) { - for b.Loop() { - _, err := ns.Where(context.Background(), seqAny, "key", "eq", "value") - if err != nil { - b.Fatal(err) - } - } - }) -} - func BenchmarkWhereSliceOfStructPointersWithMethod(b *testing.B) { // TstRv2 ns := newNs() diff --git a/tpl/compare/compare.go b/tpl/compare/compare.go index c5eaa87f5..efac28931 100644 --- a/tpl/compare/compare.go +++ b/tpl/compare/compare.go @@ -275,6 +275,13 @@ func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b a } } + // Fast path: both values are plain strings. + if as, aOk := a.(string); aOk { + if bs, bOk := b.(string); bOk { + return ns.compareTwoStrings(collator, as, bs) + } + } + var left, right float64 var leftStr, rightStr *string av := reflect.ValueOf(a) @@ -350,33 +357,34 @@ func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b a } } - if (ns.caseInsensitive || collator != nil) && leftStr != nil && rightStr != nil { + if leftStr != nil && rightStr != nil { + return ns.compareTwoStrings(collator, *leftStr, *rightStr) + } + + return left, right +} + +func (ns *Namespace) compareTwoStrings(collator *langs.Collator, a, b string) (float64, float64) { + if ns.caseInsensitive || collator != nil { var c int if collator != nil { - c = collator.CompareStrings(*leftStr, *rightStr) + c = collator.CompareStrings(a, b) } else { - c = compare.Strings(*leftStr, *rightStr) + c = compare.Strings(a, b) } if c < 0 { return 0, 1 } else if c > 0 { return 1, 0 - } else { - return 0, 0 } - } - - switch { - case leftStr == nil || rightStr == nil: - case *leftStr < *rightStr: - return 0, 1 - case *leftStr > *rightStr: - return 1, 0 - default: return 0, 0 } - - return left, right + if a < b { + return 0, 1 + } else if a > b { + return 1, 0 + } + return 0, 0 } func (ns *Namespace) toTimeUnix(v reflect.Value) int64 {