From 95cc42813ef97a8d9616773c0963128101ce4072 Mon Sep 17 00:00:00 2001 From: klarysz Date: Tue, 18 Jul 2023 03:33:25 +0200 Subject: [PATCH] added predicate testcase, updated velty version --- cmd/builder.go | 7 +- config/predicates.go | 13 +- .../cases/062_predicates/expect.json | 28 +---- .../cases/062_predicates/expect2.json | 1 + .../cases/062_predicates/expect3.json | 7 ++ .../cases/062_predicates/expect4.json | 17 +++ .../regression/cases/062_predicates/skip.txt | 1 - .../regression/cases/062_predicates/test.yaml | 28 +++-- .../062_predicates/vendors_predicate.sql | 7 +- e2e/local/regression/regression.yaml | 2 +- go.mod | 2 +- go.sum | 7 +- router/parameters.go | 18 +-- router/resource.go | 2 +- router/selectors.go | 14 ++- template/expand/predicate.go | 43 ++++++- template/sanitize/iterator.go | 10 +- view/predicate.go | 117 ++++++++++++++++++ view/resource.go | 33 ++++- view/schema.go | 26 ++-- view/template.go | 13 +- 21 files changed, 304 insertions(+), 92 deletions(-) create mode 100644 e2e/local/regression/cases/062_predicates/expect2.json create mode 100644 e2e/local/regression/cases/062_predicates/expect3.json create mode 100644 e2e/local/regression/cases/062_predicates/expect4.json delete mode 100644 e2e/local/regression/cases/062_predicates/skip.txt create mode 100644 view/predicate.go diff --git a/cmd/builder.go b/cmd/builder.go index 6231778c..129953dc 100644 --- a/cmd/builder.go +++ b/cmd/builder.go @@ -34,6 +34,7 @@ import ( "github.com/viant/toolbox/format" "github.com/viant/velty/ast/expr" "github.com/viant/velty/parser" + "github.com/viant/xdatly/predicate" "github.com/viant/xreflect" "go/ast" goFormat "go/format" @@ -2060,9 +2061,9 @@ func (s *Builder) readParamConfigs(cfg *option.ParameterConfig, cursor *parsly.C return err } - var namedArgs []*config.NamedArg + var namedArgs []*predicate.NamedArgument for pos, argName := range args[2:] { - namedArgs = append(namedArgs, &config.NamedArg{ + namedArgs = append(namedArgs, &predicate.NamedArgument{ Position: pos, Name: argName, }) @@ -2295,7 +2296,7 @@ func (s *Builder) extractArgs(content string) []string { result = append(result, strings.TrimSpace(arg)) default: if cursor.Pos < len(cursor.Input) { - arg := strings.TrimSpace(string(cursor.Input[cursor.Pos:])) + arg := strings.Trim(strings.TrimSpace(string(cursor.Input[cursor.Pos:])), `"'`) if len(arg) != 0 { result = append(result, arg) } diff --git a/config/predicates.go b/config/predicates.go index bb17aed1..3beeea34 100644 --- a/config/predicates.go +++ b/config/predicates.go @@ -14,7 +14,7 @@ type ( Parent string Name string Context int - Args []*NamedArg + Args []*predicate.NamedArgument } NamedArg struct { @@ -32,6 +32,15 @@ func (r PredicateRegistry) Lookup(name string) (*predicate.Template, error) { return result, nil } +func (r PredicateRegistry) Clone() PredicateRegistry { + result := PredicateRegistry{} + for key, template := range r { + result[key] = template + } + + return result +} + func NewEqualPredicate() *predicate.Template { return equalityCheckPredicate(PredicateEqual, true) } @@ -44,7 +53,7 @@ func equalityCheckPredicate(name string, equal bool) *predicate.Template { return &predicate.Template{ Name: name, - Source: " $Alias.$ColumnName " + negation + "= $criteria.AppendBinding($FilterValue)", + Source: " ${Alias}.${ColumnName} " + negation + "= $criteria.AppendBinding($FilterValue)", Args: []*predicate.NamedArgument{ { Name: "Alias", diff --git a/e2e/local/regression/cases/062_predicates/expect.json b/e2e/local/regression/cases/062_predicates/expect.json index c83b372a..c433832d 100644 --- a/e2e/local/regression/cases/062_predicates/expect.json +++ b/e2e/local/regression/cases/062_predicates/expect.json @@ -1,32 +1,6 @@ [ - { - "@indexBy@": "id" - }, { "id": 1, - "name": "Vendor 1", - "products": [ - { - "@indexBy@": "id" - }, - { - "id": 1, - "name": "V1 Product 1", - "userCreated": 1 - }, - { - "id": 2, - "name": "V1 Product 2", - "userCreated": 1 - } - ] - }, - { - "id": 2, - "name": "Vendor 2" - }, - { - "id": 3, - "name": "Vendor 3" + "name": "Vendor 1" } ] \ No newline at end of file diff --git a/e2e/local/regression/cases/062_predicates/expect2.json b/e2e/local/regression/cases/062_predicates/expect2.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/e2e/local/regression/cases/062_predicates/expect2.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/e2e/local/regression/cases/062_predicates/expect3.json b/e2e/local/regression/cases/062_predicates/expect3.json new file mode 100644 index 00000000..9ebbd54f --- /dev/null +++ b/e2e/local/regression/cases/062_predicates/expect3.json @@ -0,0 +1,7 @@ +[ + { + "id": 2, + "name": "Vendor 2", + "userUpdated": 0 + } +] \ No newline at end of file diff --git a/e2e/local/regression/cases/062_predicates/expect4.json b/e2e/local/regression/cases/062_predicates/expect4.json new file mode 100644 index 00000000..15661e34 --- /dev/null +++ b/e2e/local/regression/cases/062_predicates/expect4.json @@ -0,0 +1,17 @@ +[ + { + "id": 1, + "name": "Vendor 1", + "accountId": 100 + }, + { + "id": 2, + "name": "Vendor 2", + "accountId": 101 + }, + { + "id": 3, + "name": "Vendor 3", + "accountId": 100 + } +] \ No newline at end of file diff --git a/e2e/local/regression/cases/062_predicates/skip.txt b/e2e/local/regression/cases/062_predicates/skip.txt deleted file mode 100644 index 6d3f6659..00000000 --- a/e2e/local/regression/cases/062_predicates/skip.txt +++ /dev/null @@ -1 +0,0 @@ -WIP \ No newline at end of file diff --git a/e2e/local/regression/cases/062_predicates/test.yaml b/e2e/local/regression/cases/062_predicates/test.yaml index 6549be96..4d28fca1 100644 --- a/e2e/local/regression/cases/062_predicates/test.yaml +++ b/e2e/local/regression/cases/062_predicates/test.yaml @@ -5,15 +5,27 @@ pipeline: test: action: http/runner:send requests: - - Method: GET - URL: http://127.0.0.1:8080/v1/api/dev/vendors/ - Expect: - Code: 200 - JSONBody: $LoadJSON('${parentPath}/expect.json') - +# - Method: GET +# URL: http://127.0.0.1:8080/v1/api/dev/vendors-predicate?ID=1 +# Expect: +# Code: 200 +# JSONBody: $LoadJSON('${parentPath}/expect.json') +# +# - Method: GET +# URL: http://127.0.0.1:8080/v1/api/dev/vendors-predicate?ID=1&UserCreated=19273 +# Expect: +# Code: 200 +# JSONBody: $LoadJSON('${parentPath}/expect2.json') +# +# +# - Method: GET +# URL: http://127.0.0.1:8080/v1/api/dev/vendors-predicate?ID=1&UserCreated=19273&Name=Vendor%202 +# Expect: +# Code: 200 +# JSONBody: $LoadJSON('${parentPath}/expect3.json') - Method: GET - URL: http://127.0.0.1:8080/v1/api/meta/struct/dev/vendors/ + URL: http://127.0.0.1:8080/v1/api/dev/vendors-predicate?ID=1&UserCreated=19273&Name=Vendor%202&AccountID=100 Expect: Code: 200 - Body: $Cat('${parentPath}/expect_2.txt') + JSONBody: $LoadJSON('${parentPath}/expect4.json') diff --git a/e2e/local/regression/cases/062_predicates/vendors_predicate.sql b/e2e/local/regression/cases/062_predicates/vendors_predicate.sql index b0095e00..4f8f4d30 100644 --- a/e2e/local/regression/cases/062_predicates/vendors_predicate.sql +++ b/e2e/local/regression/cases/062_predicates/vendors_predicate.sql @@ -2,12 +2,15 @@ #set($_ = $ID(query/ID).WithPredicate(0, "equal", "t", "ID").Optional()) #set($_ = $UserCreated(query/UserCreated).WithPredicate(0, "equal", "t", "USER_CREATED").Optional()) -#set($_ = $Name(query/Name).WithPredicate(1, "equal", "t", "NAME").Optional()) +#set($_ = $Name(query/Name).WithPredicate(1, "equal", "t", "NAME").Optional()) #set($_ = $AccountID(query/AccountID).WithPredicate(1, "equal", "t", "ACCOUNT_ID").Optional()) SELECT vendor.* FROM (SELECT * FROM VENDOR t WHERE 1 = 1 - ${predicate.Builder().Or($predicate.Ctx(0, "AND"), $predicate.Ctx(1, "OR" )).Build("AND")} + ${predicate.Builder().CombineOr( + $predicate.Ctx(0, "AND"), + $predicate.Ctx(1, "OR" ) + ).Build("AND")} ) vendor \ No newline at end of file diff --git a/e2e/local/regression/regression.yaml b/e2e/local/regression/regression.yaml index 5603baf6..f3ee5c91 100644 --- a/e2e/local/regression/regression.yaml +++ b/e2e/local/regression/regression.yaml @@ -28,7 +28,7 @@ pipeline: '[]gen': '@gen' subPath: 'cases/${index}_*' - range: 62..062 + range: 1..062 template: checkSkip: action: nop diff --git a/go.mod b/go.mod index c9104c99..5a096197 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( github.com/viant/sqlx v0.9.0 github.com/viant/structql v0.4.1 github.com/viant/toolbox v0.34.6-0.20221112031702-3e7cdde7f888 - github.com/viant/velty v0.2.0 + github.com/viant/velty v0.2.1-0.20230718013203-d4e43dfe0ea9 github.com/viant/xdatly/types/custom v0.0.0-20230309034540-231985618fc7 github.com/viant/xreflect v0.3.2-0.20230703205132-5d95452045da github.com/viant/xunsafe v0.8.4 diff --git a/go.sum b/go.sum index 7d284d3b..1790be56 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,9 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -180,9 +183,11 @@ github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc= github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= @@ -332,8 +337,6 @@ github.com/viant/structql v0.4.1/go.mod h1:gHebhXqfhtTpehk9VyeDq6D1rO87hv62AgG4p github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/viant/toolbox v0.34.6-0.20221112031702-3e7cdde7f888 h1:iQ9ehV+Qev9s/L4eXFFaw3zvZVid+xTT5fW3G3ldEdk= github.com/viant/toolbox v0.34.6-0.20221112031702-3e7cdde7f888/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= -github.com/viant/velty v0.2.0 h1:JzfqeEva8MN0yaGq5ipOJuVbhniJh4+bILlh1qTc8Lo= -github.com/viant/velty v0.2.0/go.mod h1:C9LNTFXg7DR8B9+tcY2STVqiRQasDr8AZxMhy/lIpOU= github.com/viant/xdatly v0.3.1-0.20230713223438-282037388a67 h1:0T2BRrpDt2t1SqDNZBzUNMll0v66ZQAHtzNL3EnZnPY= github.com/viant/xdatly v0.3.1-0.20230713223438-282037388a67/go.mod h1:VElB75XaMgWXTSAqbwpDSlZQeavN8yvqWBnAPTyCfzI= github.com/viant/xdatly/extension v0.0.0-20230323215422-3e5c3147f0e6 h1:smo1mQEneoMzqnC0ThQpGZmx0gDywu7GFUmjab9rVYM= diff --git a/router/parameters.go b/router/parameters.go index 9d6031f8..a11d17ce 100644 --- a/router/parameters.go +++ b/router/parameters.go @@ -83,13 +83,13 @@ func (p *RequestParams) init(request *http.Request, route *Route) (string, error return "", nil } -func (p *RequestParams) queryParam(name string, defaultValue string) string { +func (p *RequestParams) queryParam(name string) (string, bool) { values, ok := p.queryIndex[name] if !ok { - return defaultValue + return "", ok } - return values[0] + return values[0], true } func (p *RequestParams) pathVariable(name string, defaultValue string) string { @@ -154,7 +154,8 @@ func (p *RequestParams) outputFormat(route *Route) string { } func (p *RequestParams) outputQueryFormat(route *Route) string { - format := strings.ToLower(p.queryParam(FormatQuery, "")) + param, _ := p.queryParam(FormatQuery) + format := strings.ToLower(param) if format == "" { format = route.Output.DataFormat } @@ -279,9 +280,10 @@ func (p *RequestParams) ExtractHttpParam(ctx context.Context, param *view.Parame func (p *RequestParams) extractHttpParam(ctx context.Context, param *view.Parameter, options []interface{}) (interface{}, error) { switch param.In.Kind { case view.KindPath: - return p.convert(ctx, p.pathVariable(param.In.Name, ""), param, options...) + return p.convert(true, p.pathVariable(param.In.Name, ""), param, options...) case view.KindQuery: - return p.convert(ctx, p.queryParam(param.In.Name, ""), param, options...) + pValue, ok := p.queryParam(param.In.Name) + return p.convert(ok, pValue, param, options...) case view.KindRequestBody: body, err := p.paramRequestBody(ctx, param, options...) if err != nil { @@ -290,9 +292,9 @@ func (p *RequestParams) extractHttpParam(ctx context.Context, param *view.Parame return body, nil case view.KindHeader: - return p.convert(ctx, p.header(param.In.Name), param, options...) + return p.convert(true, p.header(param.In.Name), param, options...) case view.KindCookie: - return p.convert(ctx, p.cookie(param.In.Name), param, options...) + return p.convert(true, p.cookie(param.In.Name), param, options...) } return nil, fmt.Errorf("unsupported param kind %v", param.In.Kind) diff --git a/router/resource.go b/router/resource.go index a989a11b..17dd0ed4 100644 --- a/router/resource.go +++ b/router/resource.go @@ -141,7 +141,7 @@ func (r *Resource) Init(ctx context.Context) error { columnsCache = r.ColumnsCache.Items } - if err := r.Resource.Init(ctx, r.Resource.TypeRegistry(), r._visitors, columnsCache, transforms); err != nil { + if err := r.Resource.Init(ctx, r.Resource.TypeRegistry(), r._visitors, columnsCache, transforms, config.Config.Predicates); err != nil { return err } diff --git a/router/selectors.go b/router/selectors.go index d5f724bc..c38d9db6 100644 --- a/router/selectors.go +++ b/router/selectors.go @@ -338,7 +338,7 @@ func (b *paramStateBuilder) populateOrderBy(ctx context.Context, selector *view. func (b *paramStateBuilder) orderByValue(ctx context.Context, details *ViewDetails, selector *view.Selector) (string, error) { param := details.View.Selector.OrderByParam value, err := b.extractParamValue(ctx, param, details, selector) - if err != nil { + if err != nil || value == nil { return "", err } @@ -373,6 +373,10 @@ func (b *paramStateBuilder) offsetValue(ctx context.Context, details *ViewDetail } func asInt(value interface{}, param *view.Parameter) (int, error) { + if value == nil { + return 0, nil + } + if actual, ok := value.(int); ok { return actual, nil } @@ -404,7 +408,7 @@ func (b *paramStateBuilder) populateFields(ctx context.Context, selector *view.S func (b *paramStateBuilder) fieldRawValue(ctx context.Context, details *ViewDetails, selector *view.Selector) (string, int32, error) { param := details.View.Selector.FieldsParam paramValue, err := b.extractParamValue(ctx, param, details, selector) - if err != nil { + if err != nil || paramValue == nil { return "", ValuesSeparator, err } @@ -456,11 +460,15 @@ func (b *paramStateBuilder) extractParamValueWithOptions(ctx context.Context, pa return transformIfNeeded(ctx, param, value, options...) } -func (p *RequestParams) convert(ctx context.Context, raw string, param *view.Parameter, options ...interface{}) (interface{}, error) { +func (p *RequestParams) convert(isSpecified bool, raw string, param *view.Parameter, options ...interface{}) (interface{}, error) { if raw == "" && param.IsRequired() { return nil, requiredParamErr(param) } + if !isSpecified { + return nil, nil + } + dateFormat := p.route.DateFormat if param.DateFormat != "" { dateFormat = param.DateFormat diff --git a/template/expand/predicate.go b/template/expand/predicate.go index bc4ef01b..4acd182e 100644 --- a/template/expand/predicate.go +++ b/template/expand/predicate.go @@ -26,7 +26,8 @@ type ( } PredicateBuilder struct { - output *strings.Builder + lastKeyword string + output *strings.Builder } ) @@ -58,7 +59,19 @@ func (p *Predicate) Ctx(ctx int, keyword string) (string, error) { return p.expand(ctx, keyword) } -func (b *PredicateBuilder) Or(fragments ...string) *PredicateBuilder { +func (b *PredicateBuilder) Combine(fragments ...string) *PredicateBuilder { + return b.combine("AND", fragments) +} + +func (b *PredicateBuilder) CombineOr(fragments ...string) *PredicateBuilder { + return b.combine("OR", fragments) +} + +func (b *PredicateBuilder) CombineAnd(fragments ...string) *PredicateBuilder { + return b.combine("AND", fragments) +} + +func (b *PredicateBuilder) combine(keyword string, fragments []string) *PredicateBuilder { builder := &strings.Builder{} for _, fragment := range fragments { if strings.TrimSpace(fragment) == "" { @@ -66,15 +79,25 @@ func (b *PredicateBuilder) Or(fragments ...string) *PredicateBuilder { } if builder.Len() > 0 { - builder.WriteString(" OR ") + builder.WriteString(" ") + builder.WriteString(keyword) + builder.WriteString(" ") } + builder.WriteString(" ( ") builder.WriteString(fragment) + builder.WriteString(" ) ") } if builder.Len() > 0 { if b.output.Len() != 0 { - b.output.WriteString(" AND ") + lastK := b.lastKeyword + if lastK == "" { + lastK = "AND" + } + b.output.WriteString(" ") + b.output.WriteString(lastK) + b.output.WriteString(" ") } b.output.WriteString(" ( ") @@ -113,7 +136,7 @@ func (p *Predicate) expand(ctx int, operator string) (string, error) { } } - value, err := predicateConfig.StateAccessor().Value(p.hasPtr) + value, err := predicateConfig.StateAccessor().Value(p.statePtr) if err != nil { return "", err } @@ -135,3 +158,13 @@ func (p *Predicate) expand(ctx int, operator string) (string, error) { return result.String(), nil } + +func (b *PredicateBuilder) And() *PredicateBuilder { + b.lastKeyword = "AND" + return b +} + +func (b *PredicateBuilder) Or() *PredicateBuilder { + b.lastKeyword = "OR" + return b +} diff --git a/template/sanitize/iterator.go b/template/sanitize/iterator.go index ca5833b3..56eaec00 100644 --- a/template/sanitize/iterator.go +++ b/template/sanitize/iterator.go @@ -135,10 +135,12 @@ outer: it.buildContexts(context, fnName, asSlice.X, asSlice.Y) } - xSelect, ok = asFunc.X.(*expr.Select) - if ok { - actual = xSelect - continue + if asFunc != nil && asFunc.X != nil { + xSelect, ok = asFunc.X.(*expr.Select) + if ok { + actual = xSelect + continue + } } continue outer diff --git a/view/predicate.go b/view/predicate.go new file mode 100644 index 00000000..6d741dbb --- /dev/null +++ b/view/predicate.go @@ -0,0 +1,117 @@ +package view + +import ( + "fmt" + "github.com/viant/datly/config" + "github.com/viant/datly/template/expand" + "github.com/viant/datly/utils/types" + "github.com/viant/xdatly/predicate" + "github.com/viant/xreflect" + "github.com/viant/xunsafe" + "reflect" + "sync" +) + +type ( + predicateCache struct { + sync.Map + } + + predicateKey struct { + name string + paramType reflect.Type + } + + predicateEvaluatorProvider struct { + evaluator *expand.Evaluator + ctxType reflect.Type + argsIndex map[int]*predicate.NamedArgument + stateName string + } + + predicateEvaluator struct { + ctx *expand.CustomContext + evaluator *expand.Evaluator + stateName string + } +) + +func (e *predicateEvaluator) Evaluate(ctx *expand.Context, paramValue interface{}) (*expand.State, error) { + return e.evaluator.Evaluate(ctx, expand.WithParameters(paramValue, nil), expand.WithCustomContext(e.ctx)) +} + +func (c *predicateCache) get(predicateConfig *config.PredicateConfig, paramType reflect.Type, registry config.PredicateRegistry) (*predicateEvaluator, error) { + aKey := predicateKey{name: predicateConfig.Name, paramType: paramType} + provider, err := c.getEvaluatorProvider(predicateConfig, paramType, registry, aKey) + if err != nil { + return nil, err + } + + return provider.new(predicateConfig) +} + +func (c *predicateCache) getEvaluatorProvider(predicateConfig *config.PredicateConfig, paramType reflect.Type, registry config.PredicateRegistry, aKey predicateKey) (*predicateEvaluatorProvider, error) { + value, ok := c.Map.Load(aKey) + if ok { + return value.(*predicateEvaluatorProvider), nil + } + + p := &predicateEvaluatorProvider{stateName: "FilterValue"} + err := p.init(predicateConfig, paramType, registry) + if err != nil { + return nil, err + } + + c.Map.Store(aKey, p) + return p, nil +} + +func (p *predicateEvaluatorProvider) new(predicateConfig *config.PredicateConfig) (*predicateEvaluator, error) { + dst := types.NewValue(p.ctxType) + dstPtr := xunsafe.AsPointer(dst) + for _, field := range predicateConfig.Args { + argument, ok := p.argsIndex[field.Position] + if !ok { + return nil, fmt.Errorf("not found predicate arg %v", field.Position) + } + xunsafe.FieldByName(p.ctxType, argument.Name).SetString(dstPtr, field.Name) + } + + customCtx := &expand.CustomContext{ + Type: p.ctxType, + Value: dst, + } + + return &predicateEvaluator{ + ctx: customCtx, + evaluator: p.evaluator, + stateName: p.stateName, + }, nil +} + +func (p *predicateEvaluatorProvider) init(predicateConfig *config.PredicateConfig, paramType reflect.Type, registry config.PredicateRegistry) error { + lookup, err := registry.Lookup(predicateConfig.Name) + if err != nil { + return err + } + + var ctxFields []reflect.StructField + argsIndexed := map[int]*predicate.NamedArgument{} + for _, arg := range lookup.Args { + ctxFields = append(ctxFields, newField("", arg.Name, xreflect.StringType)) + argsIndexed[arg.Position] = arg + } + + ctxType := reflect.StructOf(ctxFields) + stateName := "FilterValue" + evaluator, err := expand.NewEvaluator(lookup.Source, expand.WithStateName(stateName), expand.WithParamSchema(paramType, nil), expand.WithCustomContexts(&expand.CustomContext{Type: ctxType})) + if err != nil { + return err + } + + p.ctxType = ctxType + p.evaluator = evaluator + p.argsIndex = argsIndexed + p.stateName = stateName + return nil +} diff --git a/view/resource.go b/view/resource.go index 6f9f79a6..d6306bac 100644 --- a/view/resource.go +++ b/view/resource.go @@ -12,6 +12,7 @@ import ( "github.com/viant/datly/logger" "github.com/viant/datly/router/marshal" "github.com/viant/toolbox" + "github.com/viant/xdatly/predicate" "github.com/viant/xreflect" "gopkg.in/yaml.v3" "reflect" @@ -49,6 +50,9 @@ type Resource struct { _visitors config.CodecsRegistry ModTime time.Time `json:",omitempty"` + Templates []*predicate.Template + _templates map[string]*predicate.Template + _columnsCache map[string]Columns fs afs.Service } @@ -242,8 +246,7 @@ func (r *Resource) GetConnectors() Connectors { // Init initializes Resource func (r *Resource) Init(ctx context.Context, options ...interface{}) error { - - types, visitors, cache, transforms := r.readOptions(options) + types, visitors, cache, transforms, predicates := r.readOptions(options) r.indexProviders() r._visitors = visitors r._columnsCache = cache @@ -267,6 +270,9 @@ func (r *Resource) Init(ctx context.Context, options ...interface{}) error { } } + if err := r.initTemplates(predicates); err != nil { + } + var err error r._views, err = ViewSlice(r.Views).Index() if err != nil { @@ -293,11 +299,12 @@ func (r *Resource) Init(ctx context.Context, options ...interface{}) error { return nil } -func (r *Resource) readOptions(options []interface{}) (*xreflect.Types, config.CodecsRegistry, map[string]Columns, marshal.TransformIndex) { +func (r *Resource) readOptions(options []interface{}) (*xreflect.Types, config.CodecsRegistry, map[string]Columns, marshal.TransformIndex, config.PredicateRegistry) { var types *xreflect.Types var visitors = config.CodecsRegistry{} var cache map[string]Columns var transformsIndex marshal.TransformIndex + var predicatesRegistry config.PredicateRegistry for _, option := range options { if option == nil { @@ -312,10 +319,12 @@ func (r *Resource) readOptions(options []interface{}) (*xreflect.Types, config.C types = actual case marshal.TransformIndex: transformsIndex = actual + case config.PredicateRegistry: + predicatesRegistry = actual } } - return types, visitors, cache, transformsIndex + return types, visitors, cache, transformsIndex, predicatesRegistry } // View returns View with given name @@ -607,3 +616,19 @@ func (r *Resource) mergeMessageBuses(resource *Resource) { } } } + +func (r *Resource) initTemplates(registry config.PredicateRegistry) error { + if registry != nil { + r._templates = registry.Clone() + } + + if r._templates == nil { + r._templates = map[string]*predicate.Template{} + } + + for _, template := range r.Templates { + r._templates[template.Name] = template + } + + return nil +} diff --git a/view/schema.go b/view/schema.go index 03d7271d..a20df5e3 100644 --- a/view/schema.go +++ b/view/schema.go @@ -12,7 +12,7 @@ import ( "strings" ) -//Schema represents View as Go type. +// Schema represents View as Go type. type Schema struct { Package string `json:",omitempty" yaml:"package,omitempty"` Name string `json:",omitempty" yaml:"name,omitempty"` @@ -45,7 +45,7 @@ func NewSchema(compType reflect.Type) *Schema { return result } -//Type returns struct type +// Type returns struct type func (c *Schema) Type() reflect.Type { return c.compType } @@ -69,7 +69,7 @@ func (c *Schema) updateSliceType() { c.sliceType = c.slice.Type } -//Init build struct type +// Init build struct type func (c *Schema) Init(resource *Resource, viewCaseFormat format.Case, options ...interface{}) error { var columns []*Column var relations []*Relation @@ -178,7 +178,7 @@ func (c *Schema) initByColumns(columns []*Column, relations []*Relation, selfRef aTag += fmt.Sprintf(` velty:"names=%v"`, names) } - aField := c.newField(aTag, columnName, viewCaseFormat, rType) + aField := newCasedField(aTag, columnName, viewCaseFormat, rType) structFields = append(structFields, aField) } @@ -221,20 +221,24 @@ func (c *Schema) initByColumns(columns []*Column, relations []*Relation, selfRef } tag := `json:",omitempty" yaml:",omitempty" sqlx:"-"` - structFields = append(structFields, c.newField(tag, meta.Name, format.CaseUpperCamel, metaType)) + structFields = append(structFields, newCasedField(tag, meta.Name, format.CaseUpperCamel, metaType)) } } if selfRef != nil { - structFields = append(structFields, c.newField("", selfRef.Holder, format.CaseUpperCamel, reflect.SliceOf(ast.InterfaceType))) + structFields = append(structFields, newCasedField("", selfRef.Holder, format.CaseUpperCamel, reflect.SliceOf(ast.InterfaceType))) } structType := reflect.PtrTo(reflect.StructOf(structFields)) c.SetType(structType) } -func (c *Schema) newField(aTag string, columnName string, sourceCaseFormat format.Case, rType reflect.Type) reflect.StructField { +func newCasedField(aTag string, columnName string, sourceCaseFormat format.Case, rType reflect.Type) reflect.StructField { structFieldName := StructFieldName(sourceCaseFormat, columnName) + return newField(aTag, structFieldName, rType) +} + +func newField(aTag string, structFieldName string, rType reflect.Type) reflect.StructField { var fieldPkgPath string if structFieldName[0] < 'A' || structFieldName[0] > 'Z' { fieldPkgPath = pkgPath @@ -282,17 +286,17 @@ func createDefaultTagIfNeeded(column *Column) string { return json.DefaultTagName + `:"` + strings.Join(attributes, ",") + `"` } -//AutoGen indicates whether Schema was generated using ColumnTypes fetched from DB or was passed programmatically. +// AutoGen indicates whether Schema was generated using ColumnTypes fetched from DB or was passed programmatically. func (c *Schema) AutoGen() bool { return c.autoGen } -//Slice returns slice as xunsafe.Slice +// Slice returns slice as xunsafe.Slice func (c *Schema) Slice() *xunsafe.Slice { return c.slice } -//SliceType returns reflect.SliceOf() Schema type +// SliceType returns reflect.SliceOf() Schema type func (c *Schema) SliceType() reflect.Type { return c.sliceType } @@ -302,7 +306,7 @@ func (c *Schema) inheritType(rType reflect.Type) { c.autoGen = false } -//XType returns structType as *xunsafe.Type +// XType returns structType as *xunsafe.Type func (c *Schema) XType() *xunsafe.Type { return c.xType } diff --git a/view/template.go b/view/template.go index a0c49808..7c1a593d 100644 --- a/view/template.go +++ b/view/template.go @@ -3,7 +3,6 @@ package view import ( "context" "fmt" - "github.com/viant/datly/config" "github.com/viant/datly/executor/session" "github.com/viant/datly/shared" "github.com/viant/datly/template/expand" @@ -18,6 +17,7 @@ import ( "reflect" "sort" "strings" + "sync" ) var boolType = reflect.TypeOf(true) @@ -304,7 +304,6 @@ func Evaluate(evaluator *expand.Evaluator, options ...expand.StateOption) (*expa return evaluator.Evaluate(nil, options..., ) - } func AsViewParam(aView *View, aSelector *Selector, batchData *BatchData, options ...interface{}) *expand.MetaParam { @@ -335,18 +334,14 @@ func (t *Template) initSqlEvaluator(resource *Resource) error { return nil } + cache := &predicateCache{Map: sync.Map{}} var predicates []*expand.PredicateConfig for _, p := range t.Parameters { if p.Predicate == nil { continue } - lookup, err := config.Config.Predicates.Lookup(p.Predicate.Name) - if err != nil { - return err - } - - evaluator, err := expand.NewEvaluator(lookup.Source, expand.WithParamSchema(p.ActualParamType(), nil), expand.WithStateName("FilterValue")) + evaluator, err := cache.get(p.Predicate, p.ActualParamType(), resource._templates) if err != nil { return err } @@ -356,7 +351,7 @@ func (t *Template) initSqlEvaluator(resource *Resource) error { StateAccessor: p.ValueAccessor, HasAccessor: p.PresenceAccessor, Expander: func(c *expand.Context, i interface{}) (*parameter2.Criteria, error) { - evaluate, err := evaluator.Evaluate(c, expand.WithParameters(i, nil)) + evaluate, err := evaluator.Evaluate(c, i) if err != nil { return nil, err }