Skip to content

Commit

Permalink
Support bound parameters in the parser
Browse files Browse the repository at this point in the history
The parser can be passed a map of keys to literal values to be replaced
into the query. Parameters are preceded by a dollar sign (`$`). If a
parameter key is missing, an error is thrown by the parser.

Fixes #2926.
  • Loading branch information
jsternberg committed May 19, 2016
1 parent 5248e38 commit 451a520
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 24 additions & 1 deletion influxql/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@ const (

// Parser represents an InfluxQL parser.
type Parser struct {
s *bufScanner
s *bufScanner
params map[string]interface{}
}

// NewParser returns a new instance of Parser.
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() }

Expand Down Expand Up @@ -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)
}
Expand Down
34 changes: 29 additions & 5 deletions influxql/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions influxql/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 '*':
Expand Down
2 changes: 2 additions & 0 deletions influxql/scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
1 change: 1 addition & 0 deletions influxql/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions services/httpd/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(&params); 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 {
Expand Down

0 comments on commit 451a520

Please sign in to comment.