3
0
Files
corteza/pkg/dal/runner_gval.go
Tomaž Jerman 1d7f82172e Refactor execution steps to offload more state preparation
Extract most validation/initialization/preparation logic from
pipeline execution steps into definition steps.
2022-09-19 16:22:37 +02:00

363 lines
8.2 KiB
Go

package dal
import (
"context"
"fmt"
"strings"
"github.com/PaesslerAG/gval"
"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
}
)
var (
globalGvalConverter converterGval
)
var (
refToGvalExp = map[string]*exprHandlerGval{
// operators
"not": {
Handler: func(args ...string) string {
return fmt.Sprintf("(! %s)", args[0])
},
},
// - bool
"and": {
Handler: func(args ...string) string {
return strings.Join(args, " && ")
},
},
"or": {
Handler: func(args ...string) string {
return strings.Join(args, " || ")
},
},
"xor": {
Handler: func(args ...string) string {
return fmt.Sprintf("(%s != %s)", args[0], args[1])
},
},
// - comp.
"eq": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s == %s", args[0], args[1])
},
},
"ne": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s != %s", args[0], args[1])
},
},
"lt": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s < %s", args[0], args[1])
},
},
"le": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s <= %s", args[0], args[1])
},
},
"gt": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s > %s", args[0], args[1])
},
},
"ge": {
//Handler: makeGenericCompHandler(">="),
Handler: func(args ...string) string {
return fmt.Sprintf("%s >= %s", args[0], args[1])
},
},
// - math
"add": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s + %s", args[0], args[1])
},
},
"sub": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s - %s", args[0], args[1])
},
},
"mult": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s * %s", args[0], args[1])
},
},
"div": {
Handler: func(args ...string) string {
return fmt.Sprintf("%s / %s", args[0], args[1])
},
},
// - strings
"concat": {
Handler: func(args ...string) string {
return fmt.Sprintf("concat(%s)", strings.Join(args, ", "))
},
},
// @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()"
},
},
"quarter": {
Handler: func(args ...string) string {
return fmt.Sprintf("quarter(%s)", args[0])
},
},
"year": {
Handler: func(args ...string) string {
return fmt.Sprintf("year(%s)", args[0])
},
},
"month": {
Handler: func(args ...string) string {
return fmt.Sprintf("month(%s)", args[0])
},
},
"date": {
Handler: func(args ...string) string {
return fmt.Sprintf("date(%s)", args[0])
},
},
"date_format": {
Handler: func(args ...string) string {
return fmt.Sprintf("strftime(%s, %s)", args[0], args[1])
},
},
// generic stuff
"null": {
Handler: func(args ...string) string {
return fmt.Sprintf("isNil(%s)", args[0])
},
},
"nnull": {
Handler: func(args ...string) string {
return fmt.Sprintf("!isNil(%s)", args[0])
},
},
"exists": {
Handler: func(args ...string) string {
return fmt.Sprintf("!isNil(%s)", args[0])
},
},
// - typecast
"float": {
Handler: func(args ...string) string {
return fmt.Sprintf("float(%s)", args[0])
},
},
"int": {
Handler: func(args ...string) string {
return fmt.Sprintf("int(%s)", args[0])
},
},
"string": {
Handler: func(args ...string) string {
return fmt.Sprintf("string(%s)", args[0])
},
},
"group": {
Handler: func(args ...string) string {
return fmt.Sprintf("(%s)", args[0])
},
},
}
)
// newConverterGval initializes a new gval exp. converter
func newConverterGval() converterGval {
if globalGvalConverter.parser == nil {
globalGvalConverter = converterGval{
parser: newQlParser(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(expr 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("isNil", gvalfnc.IsNil),
gval.Function("float", gvalfnc.CastFloat),
gval.Function("int", gvalfnc.CastInt),
gval.Function("string", gvalfnc.CastString),
gval.Function("concat", gvalfnc.ConcatStrings),
).NewEvaluable(expr)
}
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)
if onIdent != nil {
ident, err = onIdent(ident)
}
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
}