diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8d3fed1c..4e2885af2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - [#6519](https://github.com/influxdata/influxdb/issues/6519): Support cast syntax for selecting a specific type. - [#6654](https://github.com/influxdata/influxdb/pull/6654): Add new HTTP statistics to monitoring - [#6664](https://github.com/influxdata/influxdb/pull/6664): Adds monitoring statistic for on-disk shard size. +- [#2926](https://github.com/influxdata/influxdb/issues/2926): Support bound parameters in the parser. ### Bugfixes diff --git a/influxql/parser.go b/influxql/parser.go index df0c00b9b4..e8272bb928 100644 --- a/influxql/parser.go +++ b/influxql/parser.go @@ -23,7 +23,8 @@ const ( // Parser represents an InfluxQL parser. type Parser struct { - s *bufScanner + s *bufScanner + params map[string]interface{} } // NewParser returns a new instance of Parser. @@ -31,6 +32,10 @@ func NewParser(r io.Reader) *Parser { return &Parser{s: newBufScanner(r)} } +func (p *Parser) SetParams(params map[string]interface{}) { + p.params = params +} + // ParseQuery parses a query string and returns its AST representation. func ParseQuery(s string) (*Query, error) { return NewParser(strings.NewReader(s)).ParseQuery() } @@ -2430,6 +2435,24 @@ func (p *Parser) parseUnaryExpr() (Expr, error) { return nil, &ParseError{Message: err.Error(), Pos: pos} } return &RegexLiteral{Val: re}, nil + case BOUNDPARAM: + v, ok := p.params[lit] + if !ok { + return nil, fmt.Errorf("missing parameter: %s", lit) + } + + switch v := v.(type) { + case float64: + return &NumberLiteral{Val: v}, nil + case int64: + return &IntegerLiteral{Val: v}, nil + case string: + return &StringLiteral{Val: v}, nil + case bool: + return &BooleanLiteral{Val: v}, nil + default: + return nil, fmt.Errorf("unable to bind parameter with type %T", v) + } default: return nil, newParseError(tokstr(tok, lit), []string{"identifier", "string", "number", "bool"}, pos) } diff --git a/influxql/parser_test.go b/influxql/parser_test.go index ef1b0f44a5..7b0daf1b70 100644 --- a/influxql/parser_test.go +++ b/influxql/parser_test.go @@ -64,10 +64,11 @@ func TestParser_ParseStatement(t *testing.T) { now := time.Now() var tests = []struct { - skip bool - s string - stmt influxql.Statement - err string + skip bool + s string + params map[string]interface{} + stmt influxql.Statement + err string }{ // SELECT * statement { @@ -820,6 +821,25 @@ func TestParser_ParseStatement(t *testing.T) { }, }, + // SELECT statement with a bound parameter + { + s: `SELECT value FROM cpu WHERE value > $value`, + params: map[string]interface{}{ + "value": int64(2), + }, + stmt: &influxql.SelectStatement{ + IsRawQuery: true, + Fields: []*influxql.Field{{ + Expr: &influxql.VarRef{Val: "value"}}}, + Sources: []influxql.Source{&influxql.Measurement{Name: "cpu"}}, + Condition: &influxql.BinaryExpr{ + Op: influxql.GT, + LHS: &influxql.VarRef{Val: "value"}, + RHS: &influxql.IntegerLiteral{Val: 2}, + }, + }, + }, + // See issues https://github.com/influxdata/influxdb/issues/1647 // and https://github.com/influxdata/influxdb/issues/4404 // DELETE statement @@ -2199,7 +2219,11 @@ func TestParser_ParseStatement(t *testing.T) { if tt.skip { continue } - stmt, err := influxql.NewParser(strings.NewReader(tt.s)).ParseStatement() + p := influxql.NewParser(strings.NewReader(tt.s)) + if tt.params != nil { + p.SetParams(tt.params) + } + stmt, err := p.ParseStatement() // We are memoizing a field so for testing we need to... if s, ok := tt.stmt.(*influxql.SelectStatement); ok { diff --git a/influxql/scanner.go b/influxql/scanner.go index 4e3b97f8f6..d56e1b4f01 100644 --- a/influxql/scanner.go +++ b/influxql/scanner.go @@ -53,6 +53,12 @@ func (s *Scanner) Scan() (tok Token, pos Pos, lit string) { return s.scanNumber() } return DOT, pos, "" + case '$': + tok, _, lit := s.scanIdent() + if tok == IDENT { + tok = BOUNDPARAM + } + return tok, pos, lit case '+', '-': return s.scanNumber() case '*': diff --git a/influxql/scanner_test.go b/influxql/scanner_test.go index ea3e757f3d..7404908d27 100644 --- a/influxql/scanner_test.go +++ b/influxql/scanner_test.go @@ -70,6 +70,8 @@ func TestScanner_Scan(t *testing.T) { {s: `"foo\"bar\""`, tok: influxql.IDENT, lit: `foo"bar"`}, {s: `test"`, tok: influxql.BADSTRING, lit: "", pos: influxql.Pos{Line: 0, Char: 3}}, {s: `"test`, tok: influxql.BADSTRING, lit: `test`}, + {s: `$host`, tok: influxql.BOUNDPARAM, lit: `host`}, + {s: `$"host param"`, tok: influxql.BOUNDPARAM, lit: `host param`}, {s: `true`, tok: influxql.TRUE}, {s: `false`, tok: influxql.FALSE}, diff --git a/influxql/token.go b/influxql/token.go index 4992b2dd8a..53a613ddf9 100644 --- a/influxql/token.go +++ b/influxql/token.go @@ -17,6 +17,7 @@ const ( literalBeg // IDENT and the following are InfluxQL literal tokens. IDENT // main + BOUNDPARAM // $param NUMBER // 12345.67 INTEGER // 12345 DURATIONVAL // 13h diff --git a/services/httpd/handler.go b/services/httpd/handler.go index 832d39d979..04d273a631 100644 --- a/services/httpd/handler.go +++ b/services/httpd/handler.go @@ -280,6 +280,36 @@ func (h *Handler) serveQuery(w http.ResponseWriter, r *http.Request, user *meta. // Do this before anything else so a parsing error doesn't leak passwords. sanitize(r) + // Parse the parameters + rawParams := r.FormValue("params") + if rawParams != "" { + var params map[string]interface{} + decoder := json.NewDecoder(strings.NewReader(rawParams)) + decoder.UseNumber() + if err := decoder.Decode(¶ms); err != nil { + h.httpError(w, "error parsing query parameters: "+err.Error(), pretty, http.StatusBadRequest) + return + } + + // Convert json.Number into int64 and float64 values + for k, v := range params { + if v, ok := v.(json.Number); ok { + var err error + if strings.Contains(string(v), ".") { + params[k], err = v.Float64() + } else { + params[k], err = v.Int64() + } + + if err != nil { + h.httpError(w, "error parsing json value: "+err.Error(), pretty, http.StatusBadRequest) + return + } + } + } + p.SetParams(params) + } + // Parse query from query string. query, err := p.ParseQuery() if err != nil {