Files
upstream-corteza/server/pkg/dal/runner_gval.go
upstream-mirror 7e385fc126
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:22:23 +05:00

473 lines
11 KiB
Go

package dal
import (
"context"
"fmt"
"reflect"
"strings"
"github.com/PaesslerAG/gval"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/gvalfnc"
"github.com/cortezaproject/corteza/server/pkg/ql"
)
type (
runnerGval struct {
eval gval.Evaluable
}
converterGval struct {
parser *ql.Parser
}
exprHandlerGval struct {
Handler func(...string) string
// @todo temporarily added to support the current aggregate attribute type calculation
OutType Type
OutTypeUnknown bool
}
)
var (
globalGvalConverter converterGval
)
var (
refToGvalExp = map[string]*exprHandlerGval{
// operators
"not": {
Handler: func(args ...string) string {
return fmt.Sprintf("(! %s)", args[0])
},
OutType: &TypeBoolean{},
},
// - bool
"and": {
Handler: func(args ...string) string {
return strings.Join(args, " && ")
},
OutType: &TypeBoolean{},
},
"or": {
Handler: func(args ...string) string {
return strings.Join(args, " || ")
},
OutType: &TypeBoolean{},
},
"xor": {
Handler: func(args ...string) string {
return fmt.Sprintf("(%s != %s)", args[0], args[1])
},
OutType: &TypeBoolean{},
},
// - comp.
"eq": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s == %s", args[0], args[1])
},
OutType: &TypeBoolean{},
},
"ne": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s != %s", args[0], args[1])
},
OutType: &TypeBoolean{},
},
"lt": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s < %s", args[0], args[1])
},
OutType: &TypeBoolean{},
},
"le": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s <= %s", args[0], args[1])
},
OutType: &TypeBoolean{},
},
"gt": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s > %s", args[0], args[1])
},
OutType: &TypeBoolean{},
},
"ge": {
// Handler: makeGenericCompHandler(">="),
Handler: func(args ...string) string {
return fmt.Sprintf("%s >= %s", args[0], args[1])
},
OutType: &TypeBoolean{},
},
// - math
"add": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s + %s", args[0], args[1])
},
OutTypeUnknown: true,
},
"sub": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s - %s", args[0], args[1])
},
OutTypeUnknown: true,
},
"mult": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s * %s", args[0], args[1])
},
OutTypeUnknown: true,
},
"div": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s / %s", args[0], args[1])
},
OutTypeUnknown: true,
},
"in": {
Handler: func(args ...string) string {
// The arguments must be reversed!!
return fmt.Sprintf("has(%s, %s)", args[1], args[0])
},
OutTypeUnknown: true,
},
"nin": {
Handler: func(args ...string) string {
// The arguments must be reversed!!
return fmt.Sprintf("!has(%s, %s)", args[1], args[0])
},
OutTypeUnknown: true,
},
// - strings
"concat": {
Handler: func(args ...string) string {
return fmt.Sprintf("concat(%s)", strings.Join(args, ", "))
},
OutType: &TypeText{},
},
"instr": {
Handler: func(args ...string) string {
return fmt.Sprintf("instr(%s, %s)", args[0], args[1])
},
OutType: &TypeNumber{},
},
// @todo implement; the commented versions are not good enough
// "like": {
// Handler: func(args ...string) string {
// // @todo better regex construction
// nn := strings.Replace(strings.Trim(args[1], "\""), "%", ".*", -1)
// nn = strings.Replace(nn, "_", ".[1]", -1)
// return fmt.Sprintf("%s =~ ^%s$", args[0], nn)
// },
// },
// "nlike": {
// Handler: func(args ...string) string {
// // @todo better regex construction
// nn := strings.Replace(args[1], "%", ".*", -1)
// nn = strings.Replace(nn, "_", ".[1]", -1)
// return fmt.Sprintf("!(%s =~ ^%s$)", args[0], nn)
// },
// },
// "is": {
// Handler: func(args ...string) string {
// return fmt.Sprintf("%s == %s", args[0], args[1])
// }
// }
// - filtering
"now": {
Handler: func(args ...string) string {
return "now()"
},
OutType: &TypeTimestamp{},
},
"quarter": {
Handler: func(args ...string) string {
return fmt.Sprintf("quarter(%s)", args[0])
},
OutType: &TypeNumber{},
},
"year": {
Handler: func(args ...string) string {
return fmt.Sprintf("year(%s)", args[0])
},
OutType: &TypeNumber{},
},
"month": {
Handler: func(args ...string) string {
return fmt.Sprintf("month(%s)", args[0])
},
OutType: &TypeNumber{},
},
"date": {
Handler: func(args ...string) string {
return fmt.Sprintf("date(%s)", args[0])
},
OutType: &TypeDate{},
},
"day": {
Handler: func(args ...string) string {
return fmt.Sprintf("day(%s)", args[0])
},
OutType: &TypeNumber{},
},
"week": {
Handler: func(args ...string) string {
return fmt.Sprintf("week(%s)", args[0])
},
OutType: &TypeNumber{},
},
"date_format": {
Handler: func(args ...string) string {
return fmt.Sprintf("strftime(%s, %s)", args[0], args[1])
},
OutType: &TypeText{},
},
// generic stuff
"null": {
Handler: func(args ...string) string {
return fmt.Sprintf("isNil(%s)", args[0])
},
OutType: &TypeBoolean{},
},
"nnull": {
Handler: func(args ...string) string {
return fmt.Sprintf("!isNil(%s)", args[0])
},
OutType: &TypeBoolean{},
},
"exists": {
Handler: func(args ...string) string {
return fmt.Sprintf("!isNil(%s)", args[0])
},
OutType: &TypeBoolean{},
},
// - typecast
"float": {
Handler: func(args ...string) string {
return fmt.Sprintf("float(%s)", args[0])
},
OutType: &TypeNumber{},
},
"int": {
Handler: func(args ...string) string {
return fmt.Sprintf("int(%s)", args[0])
},
OutType: &TypeNumber{},
},
"string": {
Handler: func(args ...string) string {
return fmt.Sprintf("string(%s)", args[0])
},
OutType: &TypeText{},
},
"group": {
Handler: func(args ...string) string {
return fmt.Sprintf("(%s)", args[0])
},
OutTypeUnknown: true,
},
}
)
// newConverterGval initializes a new gval exp. converter
func newConverterGval(ii ...ql.IdentHandler) converterGval {
if globalGvalConverter.parser == nil {
globalGvalConverter = converterGval{
parser: newQlParser(append(
ii,
// This should always happen for gval expressions
func(i ql.Ident) (ql.Ident, error) {
i.Value = wrapNestedGvalIdent(i.Value)
return i, nil
},
)...),
}
}
return globalGvalConverter
}
// newRunnerGval initializes a new gval exp. runner from the provided expression
func newRunnerGval(expr string) (out *runnerGval, err error) {
out = &runnerGval{}
c := newConverterGval()
n, err := c.Parse(expr)
if err != nil {
return nil, err
}
expr, err = c.Convert(n)
if err != nil {
return nil, err
}
out.eval, err = newGval(expr)
return
}
// newRunnerGvalParsed initializes a new gval exp. runner from the pre-parsed expression
func newRunnerGvalParsed(n *ql.ASTNode) (out *runnerGval, err error) {
out = &runnerGval{}
c := newConverterGval()
expr, err := c.Convert(n)
if err != nil {
return
}
out.eval, err = newGval(expr)
return
}
// newGval initializes a new gval evaluatable for the provided expression
//
// The eval. includes a small subset of supported expr. functions which may be
// used in the pipeline.
//
// @note the subset is limited to simplify the (eventual) offloading to the DB.
// At some point, more functions will be supported, and the ones which can't
// be offloaded will be performed in some exec. step.
func newGval(e string) (gval.Evaluable, error) {
return gval.Full(
// Extra functions we'll need
// @note don't bring in all of the expr. pkg functions as we'll need to
// support these on the DB as well
gval.Function("now", gvalfnc.Now),
gval.Function("quarter", gvalfnc.Quarter),
gval.Function("year", gvalfnc.Year),
gval.Function("month", gvalfnc.Month),
gval.Function("strftime", gvalfnc.StrfTime),
gval.Function("date", gvalfnc.Date),
gval.Function("day", gvalfnc.Day),
gval.Function("isNil", gvalfnc.IsNil),
gval.Function("float", gvalfnc.CastFloat),
gval.Function("int", gvalfnc.CastInt),
gval.Function("string", gvalfnc.CastString),
gval.Function("concat", gvalfnc.ConcatStrings),
gval.Function("has", arrHas),
).NewEvaluable(e)
}
func newQlParser(onIdent ...ql.IdentHandler) *ql.Parser {
pp := ql.NewParser()
pp.OnIdent = func(ident ql.Ident) (_ ql.Ident, err error) {
ident.Value = NormalizeAttrNames(ident.Value)
for _, oi := range onIdent {
ident, err = oi(ident)
if err != nil {
return
}
}
return ident, err
}
return pp
}
func (e *runnerGval) Test(ctx context.Context, rows any) (bool, error) {
o, err := e.eval(ctx, rows)
if err != nil {
return false, err
}
v, ok := o.(bool)
return ok && v, nil
}
func (e *runnerGval) Eval(ctx context.Context, rows any) (any, error) {
o, err := e.eval(ctx, rows)
if err != nil {
return nil, err
}
return o, nil
}
// Parse parses the QL expression into QL ASTNodes
func (c converterGval) Parse(expr string) (*ql.ASTNode, error) {
return c.parser.Parse(expr)
}
// Convert converts the given nodes into a GVal expression
// @todo add more validation so we can potentially omit exec. error checks
func (c converterGval) Convert(n *ql.ASTNode) (expr string, err error) {
return c.convert(n)
}
func (c converterGval) convert(n *ql.ASTNode) (_ string, err error) {
switch {
case n.Symbol != "":
return n.Symbol, nil
case n.Value != nil:
// @todo I don't think this is quite ok, but it works for now so it'll do for now
if n.Value.V.Type() == "String" {
return fmt.Sprintf("\"%v\"", n.Value.V.Get()), nil
}
return fmt.Sprintf("%v", n.Value.V.Get()), nil
}
args := make([]string, len(n.Args))
for i, a := range n.Args {
args[i], err = c.convert(a)
if err != nil {
return
}
}
return c.refHandler(n, args...)
}
func (c converterGval) refHandler(n *ql.ASTNode, args ...string) (out string, err error) {
r := strings.ToLower(n.Ref)
if refToGvalExp[r] == nil {
return "", fmt.Errorf("unknown ref %q", n.Ref)
}
return refToGvalExp[r].Handler(args...), nil
}
// arrHas is a helper to assure the expr.Has always gets an array (or map) as
// the first argument
//
// @todo this is needed because how the ValueGetters returns multi-value fields so
// an edge case where a field would have [a] but here, it would be presented
// as a.
// This would become obsolete when we address the actual issue.
func arrHas(arr interface{}, vv ...interface{}) (b bool, err error) {
arr = expr.UntypedValue(arr)
if isMap(arr) {
return expr.Has(arr, vv...)
}
var (
c = reflect.ValueOf(arr)
)
switch c.Kind() {
case reflect.Slice:
return expr.Has(arr, vv...)
default:
return expr.Has([]any{arr}, vv...)
}
}
func isMap(v interface{}) bool {
return reflect.TypeOf(v).Kind() == reflect.Map
}