Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support using regexes to select fields and dimensions #7442

Merged
merged 1 commit into from
Oct 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
47 changes: 34 additions & 13 deletions influxql/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand Down Expand Up @@ -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.
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
}
})
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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()")
Expand All @@ -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()")
Expand Down Expand Up @@ -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" {
Expand Down Expand Up @@ -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" {
Expand Down
17 changes: 17 additions & 0 deletions influxql/ast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
78 changes: 58 additions & 20 deletions influxql/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down