Skip to content

Commit

Permalink
Merge pull request #13 from influxdata/js-call-valuer
Browse files Browse the repository at this point in the history
Support customizing how calls are handled in reduce and eval
  • Loading branch information
jsternberg authored Mar 12, 2018
2 parents 21ddebb + 6540eb9 commit 2438539
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 49 deletions.
159 changes: 136 additions & 23 deletions ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -3876,13 +3876,37 @@ func RewriteExpr(expr Expr, fn func(Expr) Expr) Expr {

// Eval evaluates expr against a map.
func Eval(expr Expr, m map[string]interface{}) interface{} {
eval := ValuerEval{Valuer: MapValuer(m)}
return eval.Eval(expr)
}

// MapValuer is a valuer that substitutes values for the mapped interface.
type MapValuer map[string]interface{}

// Value returns the value for a key in the MapValuer.
func (m MapValuer) Value(key string) (interface{}, bool) {
v, ok := m[key]
return v, ok
}

// ValuerEval will evaluate an expression using the Valuer.
type ValuerEval struct {
Valuer Valuer

// IntegerFloatDivision will set the eval system to treat
// a division between two integers as a floating point division.
IntegerFloatDivision bool
}

// Eval evaluates an expression and returns a value.
func (v *ValuerEval) Eval(expr Expr) interface{} {
if expr == nil {
return nil
}

switch expr := expr.(type) {
case *BinaryExpr:
return evalBinaryExpr(expr, m)
return v.evalBinaryExpr(expr)
case *BooleanLiteral:
return expr.Val
case *IntegerLiteral:
Expand All @@ -3892,21 +3916,35 @@ func Eval(expr Expr, m map[string]interface{}) interface{} {
case *UnsignedLiteral:
return expr.Val
case *ParenExpr:
return Eval(expr.Expr, m)
return v.Eval(expr.Expr)
case *RegexLiteral:
return expr.Val
case *StringLiteral:
return expr.Val
case *Call:
if valuer, ok := v.Valuer.(CallValuer); ok {
val, _ := valuer.Call(expr.Name, expr.Args)
return val
}
return nil
case *VarRef:
return m[expr.Val]
val, _ := v.Valuer.Value(expr.Val)
return val
default:
return nil
}
}

func evalBinaryExpr(expr *BinaryExpr, m map[string]interface{}) interface{} {
lhs := Eval(expr.LHS, m)
rhs := Eval(expr.RHS, m)
// EvalBool evaluates expr and returns true if result is a boolean true.
// Otherwise returns false.
func (v *ValuerEval) EvalBool(expr Expr) bool {
val, _ := v.Eval(expr).(bool)
return val
}

func (v *ValuerEval) evalBinaryExpr(expr *BinaryExpr) interface{} {
lhs := v.Eval(expr.LHS)
rhs := v.Eval(expr.RHS)
if lhs == nil && rhs != nil {
// When the LHS is nil and the RHS is a boolean, implicitly cast the
// nil to false.
Expand Down Expand Up @@ -4047,8 +4085,15 @@ func evalBinaryExpr(expr *BinaryExpr, m map[string]interface{}) interface{} {
case MUL:
return lhs * rhs
case DIV:
if v.IntegerFloatDivision {
if rhs == 0 {
return float64(0)
}
return float64(lhs) / float64(rhs)
}

if rhs == 0 {
return float64(0)
return int64(0)
}
return lhs / rhs
case MOD:
Expand Down Expand Up @@ -4439,8 +4484,10 @@ func reduceBinaryExpr(expr *BinaryExpr, valuer Valuer) Expr {
rhs := reduce(expr.RHS, valuer)

loc := time.UTC
if v, ok := valuer.(ZoneValuer); ok {
loc = v.Zone()
if valuer, ok := valuer.(ZoneValuer); ok {
if l := valuer.Zone(); l != nil {
loc = l
}
}

// Do not evaluate if one side is nil.
Expand Down Expand Up @@ -4955,18 +5002,20 @@ func reduceBinaryExprTimeLHS(op Token, lhs *TimeLiteral, rhs Expr, loc *time.Loc
}

func reduceCall(expr *Call, valuer Valuer) Expr {
// Evaluate "now()" if valuer is set.
if expr.Name == "now" && len(expr.Args) == 0 && valuer != nil {
if v, ok := valuer.Value("now()"); ok {
v, _ := v.(time.Time)
return &TimeLiteral{Val: v}
// Otherwise reduce arguments.
var args []Expr
if len(expr.Args) > 0 {
args = make([]Expr, len(expr.Args))
for i, arg := range expr.Args {
args[i] = reduce(arg, valuer)
}
}

// Otherwise reduce arguments.
args := make([]Expr, len(expr.Args))
for i, arg := range expr.Args {
args[i] = reduce(arg, valuer)
// Evaluate a function call if the valuer is a CallValuer.
if valuer, ok := valuer.(CallValuer); ok {
if v, ok := valuer.Call(expr.Name, args); ok {
return asLiteral(v)
}
}
return &Call{Name: expr.Name, Args: args}
}
Expand All @@ -4993,13 +5042,20 @@ func reduceVarRef(expr *VarRef, valuer Valuer) Expr {
}

// Return the value as a literal.
return asLiteral(v)
}

// asLiteral takes an interface and converts it into an influxql literal.
func asLiteral(v interface{}) Literal {
switch v := v.(type) {
case bool:
return &BooleanLiteral{Val: v}
case time.Duration:
return &DurationLiteral{Val: v}
case float64:
return &NumberLiteral{Val: v}
case int64:
return &IntegerLiteral{Val: v}
case string:
return &StringLiteral{Val: v}
case time.Time:
Expand All @@ -5015,9 +5071,20 @@ type Valuer interface {
Value(key string) (interface{}, bool)
}

// CallValuer implements the Call method for evaluating function calls.
type CallValuer interface {
Valuer

// Call is invoked to evaluate a function call (if possible).
Call(name string, args []Expr) (interface{}, bool)
}

// ZoneValuer is the interface that specifies the current time zone.
type ZoneValuer interface {
// Zone returns the time zone location.
Valuer

// Zone returns the time zone location. This function may return nil
// if no time zone is known.
Zone() *time.Location
}

Expand All @@ -5035,12 +5102,59 @@ func (v *NowValuer) Value(key string) (interface{}, bool) {
return nil, false
}

// Call evaluates the now() function to replace now() with the current time.
func (v *NowValuer) Call(name string, args []Expr) (interface{}, bool) {
if name == "now" && len(args) == 0 {
return v.Now, true
}
return nil, false
}

// Zone is a method that returns the time.Location.
func (v *NowValuer) Zone() *time.Location {
if v.Location != nil {
return v.Location
}
return time.UTC
return nil
}

// MultiValuer returns a Valuer that iterates over multiple Valuer instances
// to find a match.
func MultiValuer(valuers ...Valuer) Valuer {
return multiValuer(valuers)
}

type multiValuer []Valuer

func (a multiValuer) Value(key string) (interface{}, bool) {
for _, valuer := range a {
if v, ok := valuer.Value(key); ok {
return v, true
}
}
return nil, false
}

func (a multiValuer) Call(name string, args []Expr) (interface{}, bool) {
for _, valuer := range a {
if valuer, ok := valuer.(CallValuer); ok {
if v, ok := valuer.Call(name, args); ok {
return v, true
}
}
}
return nil, false
}

func (a multiValuer) Zone() *time.Location {
for _, valuer := range a {
if valuer, ok := valuer.(ZoneValuer); ok {
if v := valuer.Zone(); v != nil {
return v
}
}
}
return nil
}

// ContainsVarRef returns true if expr is a VarRef or contains one.
Expand Down Expand Up @@ -5247,10 +5361,9 @@ func getTimeRange(op Token, rhs Expr, valuer Valuer) (TimeRange, error) {
if strlit, ok := rhs.(*StringLiteral); ok {
if strlit.IsTimeLiteral() {
var loc *time.Location
if v, ok := valuer.(ZoneValuer); ok {
loc = v.Zone()
if valuer, ok := valuer.(ZoneValuer); ok {
loc = valuer.Zone()
}

t, err := strlit.ToTimeLiteral(loc)
if err != nil {
return TimeRange{}, err
Expand Down
45 changes: 19 additions & 26 deletions ast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1171,7 +1171,7 @@ func TestReduce(t *testing.T) {
for i, tt := range []struct {
in string
out string
data Valuer
data influxql.MapValuer
}{
// Number literals.
{in: `1 + 2`, out: `3`},
Expand Down Expand Up @@ -1237,21 +1237,20 @@ func TestReduce(t *testing.T) {
{in: `true + false`, out: `true + false`},

// Time literals with now().
{in: `now() + 2h`, out: `'2000-01-01T02:00:00Z'`, data: map[string]interface{}{"now()": now}},
{in: `now() / 2h`, out: `'2000-01-01T00:00:00Z' / 2h`, data: map[string]interface{}{"now()": now}},
{in: `4µ + now()`, out: `'2000-01-01T00:00:00.000004Z'`, data: map[string]interface{}{"now()": now}},
{in: `now() + 2000000000`, out: `'2000-01-01T00:00:02Z'`, data: map[string]interface{}{"now()": now}},
{in: `2000000000 + now()`, out: `'2000-01-01T00:00:02Z'`, data: map[string]interface{}{"now()": now}},
{in: `now() - 2000000000`, out: `'1999-12-31T23:59:58Z'`, data: map[string]interface{}{"now()": now}},
{in: `now() = now()`, out: `true`, data: map[string]interface{}{"now()": now}},
{in: `now() <> now()`, out: `false`, data: map[string]interface{}{"now()": now}},
{in: `now() < now() + 1h`, out: `true`, data: map[string]interface{}{"now()": now}},
{in: `now() <= now() + 1h`, out: `true`, data: map[string]interface{}{"now()": now}},
{in: `now() >= now() - 1h`, out: `true`, data: map[string]interface{}{"now()": now}},
{in: `now() > now() - 1h`, out: `true`, data: map[string]interface{}{"now()": now}},
{in: `now() - (now() - 60s)`, out: `1m`, data: map[string]interface{}{"now()": now}},
{in: `now() AND now()`, out: `'2000-01-01T00:00:00Z' AND '2000-01-01T00:00:00Z'`, data: map[string]interface{}{"now()": now}},
{in: `now()`, out: `now()`},
{in: `now() + 2h`, out: `'2000-01-01T02:00:00Z'`},
{in: `now() / 2h`, out: `'2000-01-01T00:00:00Z' / 2h`},
{in: `4µ + now()`, out: `'2000-01-01T00:00:00.000004Z'`},
{in: `now() + 2000000000`, out: `'2000-01-01T00:00:02Z'`},
{in: `2000000000 + now()`, out: `'2000-01-01T00:00:02Z'`},
{in: `now() - 2000000000`, out: `'1999-12-31T23:59:58Z'`},
{in: `now() = now()`, out: `true`},
{in: `now() <> now()`, out: `false`},
{in: `now() < now() + 1h`, out: `true`},
{in: `now() <= now() + 1h`, out: `true`},
{in: `now() >= now() - 1h`, out: `true`},
{in: `now() > now() - 1h`, out: `true`},
{in: `now() - (now() - 60s)`, out: `1m`},
{in: `now() AND now()`, out: `'2000-01-01T00:00:00Z' AND '2000-01-01T00:00:00Z'`},
{in: `946684800000000000 + 2h`, out: `'2000-01-01T02:00:00Z'`},

// Time literals.
Expand Down Expand Up @@ -1299,7 +1298,10 @@ func TestReduce(t *testing.T) {
{in: `foo <> 'bar'`, out: `false`, data: map[string]interface{}{"foo": nil}},
} {
// Fold expression.
expr := influxql.Reduce(MustParseExpr(tt.in), tt.data)
expr := influxql.Reduce(MustParseExpr(tt.in), influxql.MultiValuer(
tt.data,
&influxql.NowValuer{Now: now},
))

// Compare with expected output.
if out := expr.String(); tt.out != out {
Expand Down Expand Up @@ -1678,15 +1680,6 @@ func Test_EnforceHasDefaultDatabase(t *testing.T) {
}
}

// Valuer represents a simple wrapper around a map to implement the influxql.Valuer interface.
type Valuer map[string]interface{}

// Value returns the value and existence of a key.
func (o Valuer) Value(key string) (v interface{}, ok bool) {
v, ok = o[key]
return
}

// MustTimeRange will parse a time range. Panic on error.
func MustTimeRange(expr influxql.Expr) (min, max time.Time) {
_, timeRange, err := influxql.ConditionExpr(expr, nil)
Expand Down

0 comments on commit 2438539

Please sign in to comment.