diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f2b6d8a205..143b172ff55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - [#7388](https://github.com/influxdata/influxdb/pull/7388): Implement cumulative_sum() function. - [#7441](https://github.com/influxdata/influxdb/pull/7441): Speed up shutdown by closing shards concurrently. - [#7146](https://github.com/influxdata/influxdb/issues/7146): Add max-values-per-tag to limit high tag cardinality data +- [#5955](https://github.com/influxdata/influxdb/issues/5955): Make regex work on field and dimension keys in SELECT clause. ### Bugfixes diff --git a/influxql/ast.go b/influxql/ast.go index ff9336061dc..bbed29211b6 100644 --- a/influxql/ast.go +++ b/influxql/ast.go @@ -1128,6 +1128,12 @@ func (s *SelectStatement) RewriteFields(ic IteratorCreator) (*SelectStatement, e } rwFields = append(rwFields, &Field{Expr: &VarRef{Val: ref.Val, Type: ref.Type}}) } + case *RegexLiteral: + for _, ref := range fields { + if expr.Val.MatchString(ref.Val) { + rwFields = append(rwFields, &Field{Expr: &VarRef{Val: ref.Val, Type: ref.Type}}) + } + } case *Call: // Clone a template that we can modify and use for new fields. template := CloneExpr(expr).(*Call) @@ -1149,10 +1155,16 @@ func (s *SelectStatement) RewriteFields(ic IteratorCreator) (*SelectStatement, e continue } - wc, ok := call.Args[0].(*Wildcard) - if ok && wc.Type == TAG { - return s, fmt.Errorf("unable to use tag wildcard in %s()", call.Name) - } else if !ok { + // Retrieve if this is a wildcard or a regular expression. + var re *regexp.Regexp + switch expr := call.Args[0].(type) { + case *Wildcard: + if expr.Type == TAG { + return s, fmt.Errorf("unable to use tag wildcard in %s()", call.Name) + } + case *RegexLiteral: + re = expr.Val + default: rwFields = append(rwFields, f) continue } @@ -1181,6 +1193,8 @@ func (s *SelectStatement) RewriteFields(ic IteratorCreator) (*SelectStatement, e continue } else if _, ok := supportedTypes[ref.Type]; !ok { continue + } else if re != nil && !re.MatchString(ref.Val) { + continue } // Make a new expression and replace the wildcard within this cloned expression. @@ -1202,11 +1216,17 @@ func (s *SelectStatement) RewriteFields(ic IteratorCreator) (*SelectStatement, e // Allocate a slice assuming there is exactly one wildcard for efficiency. rwDimensions := make(Dimensions, 0, len(s.Dimensions)+len(dimensions)-1) for _, d := range s.Dimensions { - switch d.Expr.(type) { + switch expr := d.Expr.(type) { case *Wildcard: for _, name := range dimensions { rwDimensions = append(rwDimensions, &Dimension{Expr: &VarRef{Val: name}}) } + case *RegexLiteral: + for _, name := range dimensions { + if expr.Val.MatchString(name) { + rwDimensions = append(rwDimensions, &Dimension{Expr: &VarRef{Val: name}}) + } + } default: rwDimensions = append(rwDimensions, d) } @@ -1420,8 +1440,8 @@ func (s *SelectStatement) HasFieldWildcard() (hasWildcard bool) { if hasWildcard { return } - _, ok := n.(*Wildcard) - if ok { + switch n.(type) { + case *Wildcard, *RegexLiteral: hasWildcard = true } }) @@ -1432,8 +1452,8 @@ func (s *SelectStatement) HasFieldWildcard() (hasWildcard bool) { // at least 1 wildcard in the dimensions aka `GROUP BY` func (s *SelectStatement) HasDimensionWildcard() bool { for _, d := range s.Dimensions { - _, ok := d.Expr.(*Wildcard) - if ok { + switch d.Expr.(type) { + case *Wildcard, *RegexLiteral: return true } } @@ -1515,6 +1535,7 @@ func (s *SelectStatement) validateDimensions() error { return errors.New("time() is a function and expects at least one argument") } case *Wildcard: + case *RegexLiteral: default: return errors.New("only time and tag dimensions allowed") } @@ -1601,7 +1622,7 @@ func (s *SelectStatement) validPercentileAggr(expr *Call) error { } switch expr.Args[0].(type) { - case *VarRef: + case *VarRef, *RegexLiteral, *Wildcard: // do nothing default: return fmt.Errorf("expected field argument in percentile()") @@ -1625,7 +1646,7 @@ func (s *SelectStatement) validSampleAggr(expr *Call) error { } switch expr.Args[0].(type) { - case *VarRef: + case *VarRef, *RegexLiteral, *Wildcard: // do nothing default: return fmt.Errorf("expected field argument in sample()") @@ -1706,7 +1727,7 @@ func (s *SelectStatement) validateAggregates(tr targetRequirement) error { } switch fc := c.Args[0].(type) { - case *VarRef, *Wildcard: + case *VarRef, *Wildcard, *RegexLiteral: // do nothing case *Call: if fc.Name != "distinct" || expr.Name != "count" { @@ -1774,7 +1795,7 @@ func (s *SelectStatement) validateAggregates(tr targetRequirement) error { return fmt.Errorf("invalid number of arguments for %s, expected %d, got %d", expr.Name, exp, got) } switch fc := expr.Args[0].(type) { - case *VarRef, *Wildcard: + case *VarRef, *Wildcard, *RegexLiteral: // do nothing case *Call: if fc.Name != "distinct" || expr.Name != "count" { diff --git a/influxql/ast_test.go b/influxql/ast_test.go index f4862871d44..f8e41440708 100644 --- a/influxql/ast_test.go +++ b/influxql/ast_test.go @@ -390,6 +390,23 @@ func TestSelectStatement_RewriteFields(t *testing.T) { stmt: `SELECT mean(*) AS alias FROM cpu`, rewrite: `SELECT mean(value1::float) AS alias_value1, mean(value2::integer) AS alias_value2 FROM cpu`, }, + + // Query regex + { + stmt: `SELECT /1/ FROM cpu`, + rewrite: `SELECT value1::float FROM cpu`, + }, + + { + stmt: `SELECT value1 FROM cpu GROUP BY /h/`, + rewrite: `SELECT value1::float FROM cpu GROUP BY host`, + }, + + // Query regex + { + stmt: `SELECT mean(/1/) FROM cpu`, + rewrite: `SELECT mean(value1::float) AS mean_value1 FROM cpu`, + }, } for i, tt := range tests { diff --git a/influxql/parser.go b/influxql/parser.go index 0437e542d9f..0e772e0a159 100644 --- a/influxql/parser.go +++ b/influxql/parser.go @@ -1915,19 +1915,27 @@ func (p *Parser) parseFields() (Fields, error) { func (p *Parser) parseField() (*Field, error) { f := &Field{} - _, pos, _ := p.scanIgnoreWhitespace() - p.unscan() - // Parse the expression first. - expr, err := p.ParseExpr() + // Attempt to parse a regex. + re, err := p.parseRegex() if err != nil { return nil, err + } else if re != nil { + f.Expr = re + } else { + _, pos, _ := p.scanIgnoreWhitespace() + p.unscan() + // Parse the expression first. + expr, err := p.ParseExpr() + if err != nil { + return nil, err + } + var c validateField + Walk(&c, expr) + if c.foundInvalid { + return nil, fmt.Errorf("invalid operator %s in SELECT clause at line %d, char %d; operator is intended for WHERE clause", c.badToken, pos.Line+1, pos.Char+1) + } + f.Expr = expr } - var c validateField - Walk(&c, expr) - if c.foundInvalid { - return nil, fmt.Errorf("invalid operator %s in SELECT clause at line %d, char %d; operator is intended for WHERE clause", c.badToken, pos.Line+1, pos.Char+1) - } - f.Expr = expr // Parse the alias if the current and next tokens are "WS AS". alias, err := p.parseAlias() @@ -2115,6 +2123,13 @@ func (p *Parser) parseDimensions() (Dimensions, error) { // parseDimension parses a single dimension. func (p *Parser) parseDimension() (*Dimension, error) { + re, err := p.parseRegex() + if err != nil { + return nil, err + } else if re != nil { + return &Dimension{Expr: re}, nil + } + // Parse the expression first. expr, err := p.ParseExpr() if err != nil { @@ -2538,27 +2553,50 @@ func (p *Parser) parseRegex() (*RegexLiteral, error) { // This function assumes the function name and LPAREN have been consumed. func (p *Parser) parseCall(name string) (*Call, error) { name = strings.ToLower(name) - // If there's a right paren then just return immediately. - if tok, _, _ := p.scan(); tok == RPAREN { - return &Call{Name: name}, nil - } - p.unscan() - // Otherwise parse function call arguments. + // Parse first function argument if one exists. var args []Expr - for { - // Parse an expression argument. + re, err := p.parseRegex() + if err != nil { + return nil, err + } else if re != nil { + args = append(args, re) + } else { + // If there's a right paren then just return immediately. + if tok, _, _ := p.scan(); tok == RPAREN { + return &Call{Name: name}, nil + } + p.unscan() + arg, err := p.ParseExpr() if err != nil { return nil, err } args = append(args, arg) + } - // If there's not a comma next then stop parsing arguments. - if tok, _, _ := p.scan(); tok != COMMA { + // Parse additional function arguments if there is a comma. + for { + // If there's not a comma, stop parsing arguments. + if tok, _, _ := p.scanIgnoreWhitespace(); tok != COMMA { p.unscan() break } + + re, err := p.parseRegex() + if err != nil { + return nil, err + } else if re != nil { + args = append(args, re) + continue + } + + // Parse an expression argument. + arg, err := p.ParseExpr() + if err != nil { + return nil, err + } + args = append(args, arg) } // There should be a right parentheses at the end.