From 42581c74326e7e285a5e0e6dd744e26e0f0870a5 Mon Sep 17 00:00:00 2001 From: Tom Young Date: Thu, 22 Mar 2018 12:58:48 +0000 Subject: [PATCH] Add new math functions: - abs - asin, acos, atan, atan2 - exp, ln, log, log2, log10 - pow, sqrt --- query/compile.go | 50 ++++++++++++++++--- query/compile_test.go | 38 ++++++++++++++ query/math.go | 113 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 181 insertions(+), 20 deletions(-) diff --git a/query/compile.go b/query/compile.go index 216c8126233..d06c54762a2 100644 --- a/query/compile.go +++ b/query/compile.go @@ -250,7 +250,7 @@ func (c *compiledField) compileExpr(expr influxql.Expr) error { return nil case *influxql.Call: if isMathFunction(expr) { - return c.compileTrigFunction(expr) + return c.compileMathFunction(expr) } // Register the function call in the list of function calls. @@ -668,11 +668,29 @@ func (c *compiledField) compileTopBottom(call *influxql.Call) error { return nil } -func (c *compiledField) compileTrigFunction(expr *influxql.Call) error { - if exp, got := 1, len(expr.Args); exp != got { - return fmt.Errorf("invalid number of arguments for %s, expected %d, got %d", expr.Name, exp, got) +func (c *compiledField) compileMathFunction(expr *influxql.Call) error { + // How many arguments are we expecting? + nargs := 1 + switch expr.Name { + case "atan2", "pow", "log": + nargs = 2 + } + + // Did we get the expected number of args? + if got := len(expr.Args); got != nargs { + return fmt.Errorf("invalid number of arguments for %s, expected %d, got %d", expr.Name, nargs, got) + } + + // Compile all the argument expressions that are not just literals. + for _, arg := range expr.Args { + if _, ok := arg.(influxql.Literal); ok { + continue + } + if err := c.compileExpr(arg); err != nil { + return err + } } - return c.compileExpr(expr.Args[0]) + return nil } func (c *compiledStatement) compileDimensions(stmt *influxql.SelectStatement) error { @@ -801,10 +819,26 @@ func (c *compiledStatement) validateCondition(expr influxql.Expr) error { if !isMathFunction(expr) { return fmt.Errorf("invalid function call in condition: %s", expr) } - if exp, got := 1, len(expr.Args); exp != got { - return fmt.Errorf("invalid number of arguments for %s, expected %d, got %d", expr.Name, exp, got) + + // How many arguments are we expecting? + nargs := 1 + switch expr.Name { + case "atan2", "pow": + nargs = 2 + } + + // Did we get the expected number of args? + if got := len(expr.Args); got != nargs { + return fmt.Errorf("invalid number of arguments for %s, expected %d, got %d", expr.Name, nargs, got) + } + + // Are all the args valid? + for _, arg := range expr.Args { + if err := c.validateCondition(arg); err != nil { + return err + } } - return c.validateCondition(expr.Args[0]) + return nil default: return nil } diff --git a/query/compile_test.go b/query/compile_test.go index be622d96d1a..6f5a3884dd5 100644 --- a/query/compile_test.go +++ b/query/compile_test.go @@ -82,6 +82,29 @@ func TestCompile_Success(t *testing.T) { `SELECT value FROM (SELECT value FROM cpu) ORDER BY time DESC`, `SELECT count(distinct(value)), max(value) FROM cpu`, `SELECT last(value) / (1 - 0) FROM cpu`, + `SELECT abs(value) FROM cpu`, + `SELECT sin(value) FROM cpu`, + `SELECT cos(value) FROM cpu`, + `SELECT tan(value) FROM cpu`, + `SELECT asin(value) FROM cpu`, + `SELECT acos(value) FROM cpu`, + `SELECT atan(value) FROM cpu`, + `SELECT sqrt(value) FROM cpu`, + `SELECT pow(value, 2) FROM cpu`, + `SELECT pow(value, 3.14) FROM cpu`, + `SELECT pow(2, value) FROM cpu`, + `SELECT pow(3.14, value) FROM cpu`, + `SELECT exp(value) FROM cpu`, + `SELECT atan2(value, 0.1) FROM cpu`, + `SELECT atan2(0.2, value) FROM cpu`, + `SELECT atan2(value, 1) FROM cpu`, + `SELECT atan2(2, value) FROM cpu`, + `SELECT ln(value) FROM cpu`, + `SELECT log(value, 2) FROM cpu`, + `SELECT log2(value) FROM cpu`, + `SELECT log10(value) FROM cpu`, + `SELECT sin(value) - sin(1.3) FROM cpu`, + `SELECT value FROM cpu WHERE sin(value) > 0.5`, } { t.Run(tt, func(t *testing.T) { stmt, err := influxql.ParseStatement(tt) @@ -318,6 +341,21 @@ func TestCompile_Failures(t *testing.T) { {s: `SELECT value FROM myseries WHERE value OR time >= now() - 1m`, err: `invalid condition expression: value`}, {s: `SELECT value FROM myseries WHERE time >= now() - 1m OR value`, err: `invalid condition expression: value`}, {s: `SELECT value FROM (SELECT value FROM cpu ORDER BY time DESC) ORDER BY time ASC`, err: `subqueries must be ordered in the same direction as the query itself`}, + {s: `SELECT sin(value, 3) FROM cpu`, err: `invalid number of arguments for sin, expected 1, got 2`}, + {s: `SELECT cos(2.3, value, 3) FROM cpu`, err: `invalid number of arguments for cos, expected 1, got 3`}, + {s: `SELECT tan(value, 3) FROM cpu`, err: `invalid number of arguments for tan, expected 1, got 2`}, + {s: `SELECT asin(value, 3) FROM cpu`, err: `invalid number of arguments for asin, expected 1, got 2`}, + {s: `SELECT acos(value, 3.2) FROM cpu`, err: `invalid number of arguments for acos, expected 1, got 2`}, + {s: `SELECT atan() FROM cpu`, err: `invalid number of arguments for atan, expected 1, got 0`}, + {s: `SELECT sqrt(42, 3, 4) FROM cpu`, err: `invalid number of arguments for sqrt, expected 1, got 3`}, + {s: `SELECT abs(value, 3) FROM cpu`, err: `invalid number of arguments for abs, expected 1, got 2`}, + {s: `SELECT ln(value, 3) FROM cpu`, err: `invalid number of arguments for ln, expected 1, got 2`}, + {s: `SELECT log2(value, 3) FROM cpu`, err: `invalid number of arguments for log2, expected 1, got 2`}, + {s: `SELECT log10(value, 3) FROM cpu`, err: `invalid number of arguments for log10, expected 1, got 2`}, + {s: `SELECT pow(value, 3, 3) FROM cpu`, err: `invalid number of arguments for pow, expected 2, got 3`}, + {s: `SELECT atan2(value, 3, 3) FROM cpu`, err: `invalid number of arguments for atan2, expected 2, got 3`}, + {s: `SELECT sin(1.3) FROM cpu`, err: `field must contain at least one variable`}, + {s: `SELECT nofunc(1.3) FROM cpu`, err: `undefined function nofunc()`}, } { t.Run(tt.s, func(t *testing.T) { stmt, err := influxql.ParseStatement(tt.s) diff --git a/query/math.go b/query/math.go index 467fc22c55f..c9f266ba62d 100644 --- a/query/math.go +++ b/query/math.go @@ -9,7 +9,7 @@ import ( func isMathFunction(call *influxql.Call) bool { switch call.Name { - case "sin", "cos", "tan", "floor", "ceil", "round": + case "abs", "sin", "cos", "tan", "asin", "acos", "atan", "atan2", "exp", "log", "ln", "log2", "log10", "sqrt", "pow", "floor", "ceil", "round": return true } return false @@ -23,7 +23,7 @@ func (MathTypeMapper) MapType(measurement *influxql.Measurement, field string) i func (MathTypeMapper) CallType(name string, args []influxql.DataType) (influxql.DataType, error) { switch name { - case "sin", "cos", "tan": + case "abs", "sin", "cos", "tan", "asin", "acos", "atan", "atan2", "exp", "log", "ln", "log2", "log10", "sqrt", "pow": return influxql.Float, nil case "floor", "ceil", "round": switch args[0] { @@ -48,12 +48,30 @@ func (v MathValuer) Call(name string, args []interface{}) (interface{}, bool) { if len(args) == 1 { arg0 := args[0] switch name { + case "abs": + switch arg0 := arg0.(type) { + case float64: + return math.Abs(arg0), true + case int64, uint64: + return arg0, true + default: + return nil, true + } case "sin": - return v.callTrigFunction(math.Sin, arg0) + if arg0, ok := asFloat(arg0); ok { + return math.Sin(arg0), true + } + return nil, true case "cos": - return v.callTrigFunction(math.Cos, arg0) + if arg0, ok := asFloat(arg0); ok { + return math.Cos(arg0), true + } + return nil, true case "tan": - return v.callTrigFunction(math.Tan, arg0) + if arg0, ok := asFloat(arg0); ok { + return math.Tan(arg0), true + } + return nil, true case "floor": switch arg0 := arg0.(type) { case float64: @@ -81,22 +99,93 @@ func (v MathValuer) Call(name string, args []interface{}) (interface{}, bool) { default: return nil, true } + case "asin": + if arg0, ok := asFloat(arg0); ok { + return math.Asin(arg0), true + } + return nil, true + case "acos": + if arg0, ok := asFloat(arg0); ok { + return math.Acos(arg0), true + } + return nil, true + case "atan": + if arg0, ok := asFloat(arg0); ok { + return math.Atan(arg0), true + } + return nil, true + case "exp": + if arg0, ok := asFloat(arg0); ok { + return math.Exp(arg0), true + } + return nil, true + case "ln": + if arg0, ok := asFloat(arg0); ok { + return math.Log(arg0), true + } + return nil, true + case "log2": + if arg0, ok := asFloat(args); ok { + return math.Log2(arg0), true + } + return nil, true + case "log10": + if arg0, ok := asFloat(arg0); ok { + return math.Log10(arg0), true + } + return nil, true + case "sqrt": + if arg0, ok := asFloat(arg0); ok { + return math.Sqrt(arg0), true + } + return nil, true + } + } else if len(args) == 2 { + arg0, arg1 := args[0], args[1] + switch name { + case "atan2": + if arg0, arg1, ok := asFloats(arg0, arg1); ok { + return math.Atan2(arg0, arg1), true + } + return nil, true + case "log": + if arg0, arg1, ok := asFloats(arg0, arg1); ok { + return math.Log(arg0) / math.Log(arg1), true + } + return nil, true + case "pow": + if arg0, arg1, ok := asFloats(arg0, arg1); ok { + return math.Pow(arg0, arg1), true + } + return nil, true } } return nil, false } -func (MathValuer) callTrigFunction(fn func(x float64) float64, arg0 interface{}) (interface{}, bool) { - var value float64 - switch arg0 := arg0.(type) { +func asFloat(x interface{}) (float64, bool) { + switch arg0 := x.(type) { case float64: - value = arg0 + return arg0, true case int64: - value = float64(arg0) + return float64(arg0), true + case uint64: + return float64(arg0), true default: - return nil, false + return 0, false + } +} + +func asFloats(x, y interface{}) (float64, float64, bool) { + arg0, ok := asFloat(x) + if !ok { + return 0, 0, false + } + arg1, ok := asFloat(y) + if !ok { + return 0, 0, false } - return fn(value), true + return arg0, arg1, true } func round(x float64) float64 {