influxql: change ORDER BY to accept a field list

Change ORDER BY to accept a field list and change LIST SERIES
statement to allow ORDER BY.
pull/1222/head
David Norton 2014-12-14 19:54:51 -05:00
parent 0f3ea136be
commit 6f3ba3efdb
4 changed files with 160 additions and 190 deletions

View File

@ -72,8 +72,10 @@ func (_ *DurationLiteral) node() {}
func (_ *BinaryExpr) node() {}
func (_ *ParenExpr) node() {}
func (_ *Wildcard) node() {}
func (_ SortFields) node() {}
func (_ *SortField) node() {}
// Query represents a collection of order statements.
// Query represents a collection of ordered statements.
type Query struct {
Statements Statements
}
@ -84,7 +86,7 @@ func (q *Query) String() string { return q.Statements.String() }
// Statements represents a list of statements.
type Statements []Statement
// String returns a string representation of the statements
// String returns a string representation of the statements.
func (a Statements) String() string {
var str []string
for _, stmt := range a {
@ -134,6 +136,36 @@ func (_ *Series) source() {}
func (_ *Join) source() {}
func (_ *Merge) source() {}
// SortField represens a field to sort results by.
type SortField struct {
// Name of the field
Name string
// Sort order.
Ascending bool
}
// String returns a string representation of a sort field
func (field *SortField) String() string {
var buf bytes.Buffer
_, _ = buf.WriteString(field.Name)
_, _ = buf.WriteString(" ")
_, _ = buf.WriteString(strconv.FormatBool(field.Ascending))
return buf.String()
}
// SortFields represents an ordered list of ORDER BY fields
type SortFields []*SortField
// String returns a string representation of sort fields
func (a SortFields) String() string {
fields := make([]string, 0, len(a))
for _, field := range a {
fields = append(fields, field.String())
}
return strings.Join(fields, ", ")
}
// SelectStatement represents a command for extracting data from the database.
type SelectStatement struct {
// Expressions returned from the selection.
@ -148,12 +180,12 @@ type SelectStatement struct {
// An expression evaluated on data point.
Condition Expr
// Fields to sort results by
SortFields SortFields
// Maximum number of rows to be returned.
// Unlimited if zero.
Limit int
// Sort order.
Ascending bool
}
// String returns a string representation of the select statement.
@ -174,8 +206,9 @@ func (s *SelectStatement) String() string {
if s.Limit > 0 {
_, _ = fmt.Fprintf(&buf, " LIMIT %d", s.Limit)
}
if s.Ascending {
_, _ = buf.WriteString(" ORDER BY ASC")
if len(s.SortFields) > 0 {
_, _ = buf.WriteString(" ORDER BY ")
_, _ = buf.WriteString(s.SortFields.String())
}
return buf.String()
}
@ -230,7 +263,7 @@ func (s *SelectStatement) Substatement(ref *VarRef) (*SelectStatement, error) {
Fields: Fields{{Expr: ref}},
Dimensions: s.Dimensions,
Limit: s.Limit,
Ascending: s.Ascending,
SortFields: s.SortFields,
}
// If there is only one series source then return it with the whole condition.
@ -346,6 +379,9 @@ type ListSeriesStatement struct{
// Maximum number of rows to be returned.
// Unlimited if zero.
Limit int
// Fields to sort results by
SortFields SortFields
}
// String returns a string representation of the list series statement.

View File

@ -122,6 +122,22 @@ func (p *Parser) parseSelectStatement() (*SelectStatement, error) {
}
stmt.Dimensions = dimensions
// Parse sort: "ORDER BY FIELD+".
if tok, _, _ := p.scanIgnoreWhitespace(); tok == ORDER {
if tok, pos, lit := p.scanIgnoreWhitespace(); tok != BY {
return nil, newParseError(tokstr(tok, lit), []string{"BY"}, pos)
}
sortFields, err := p.parseSortFields()
if err != nil {
return nil, err
}
stmt.SortFields = sortFields
} else {
p.unscan()
}
// Parse limit: "LIMIT INT".
limit, err := p.parseLimit()
if err != nil {
@ -129,13 +145,6 @@ func (p *Parser) parseSelectStatement() (*SelectStatement, error) {
}
stmt.Limit = limit
// Parse ordering: "ORDER BY (ASC|DESC)".
ascending, err := p.parseOrderBy()
if err != nil {
return nil, err
}
stmt.Ascending = ascending
return stmt, nil
}
@ -176,6 +185,22 @@ func (p *Parser) parseListSeriesStatement() (*ListSeriesStatement, error) {
}
stmt.Condition = condition
// Parse sort: "ORDER BY FIELD+".
if tok, _, _ := p.scanIgnoreWhitespace(); tok == ORDER {
if tok, pos, lit := p.scanIgnoreWhitespace(); tok != BY {
return nil, newParseError(tokstr(tok, lit), []string{"BY"}, pos)
}
sortFields, err := p.parseSortFields()
if err != nil {
return nil, err
}
stmt.SortFields = sortFields
} else {
p.unscan()
}
// Parse limit: "LIMIT INT".
limit, err := p.parseLimit()
if err != nil {
@ -468,26 +493,58 @@ func (p *Parser) parseLimit() (int, error) {
return int(n), nil
}
// parseOrderBy parses the "ORDER BY" clause of the query, if it exists.
func (p *Parser) parseOrderBy() (bool, error) {
// Check if the ORDER token exists.
if tok, _, _ := p.scanIgnoreWhitespace(); tok != ORDER {
p.unscan()
return false, nil
// parseSortFields parses all fields of and ORDER BY clause.
func (p *Parser) parseSortFields() (SortFields, error) {
var fields SortFields
// At least one field is required.
field, err := p.parseSortField()
if err != nil {
return nil, err
}
fields = append(fields, field)
// Parse additional fields.
for {
tok, _, _ := p.scanIgnoreWhitespace()
if tok != COMMA {
p.unscan()
break
}
field, err := p.parseSortField()
if err != nil {
return nil, err
}
fields = append(fields, field)
}
// Ensure the next token is BY.
if tok, pos, lit := p.scanIgnoreWhitespace(); tok != BY {
return false, newParseError(tokstr(tok, lit), []string{"BY"}, pos)
}
return fields, nil
}
// Ensure the next token is ASC OR DESC.
// parseSortField parses one field of an ORDER BY clause.
func (p *Parser) parseSortField() (*SortField, error) {
field := &SortField{}
// Next token should be ASC, DESC, or IDENT | STRING.
tok, pos, lit := p.scanIgnoreWhitespace()
if tok != ASC && tok != DESC {
return false, newParseError(tokstr(tok, lit), []string{"ASC", "DESC"}, pos)
if tok == IDENT || tok == STRING {
field.Name = lit
// Check for optional ASC or DESC token.
tok, pos, lit = p.scanIgnoreWhitespace()
if tok != ASC && tok != DESC {
p.unscan()
return field, nil
}
} else if tok != ASC && tok != DESC {
return nil, newParseError(tokstr(tok, lit), []string{"identifier, ASC, or DESC"}, pos)
}
return tok == ASC, nil
field.Ascending = (tok == ASC)
return field, nil
}
// ParseExpr parses an expression.

View File

@ -58,7 +58,7 @@ func TestParser_ParseStatement(t *testing.T) {
// SELECT statement
{
s: `SELECT field1, field2 ,field3 AS field_x FROM myseries WHERE host = 'hosta.influxdb.org' GROUP BY 10h LIMIT 20 ORDER BY ASC;`,
s: `SELECT field1, field2 ,field3 AS field_x FROM myseries WHERE host = 'hosta.influxdb.org' GROUP BY 10h ORDER BY ASC LIMIT 20;`,
stmt: &influxql.SelectStatement{
Fields: influxql.Fields{
&influxql.Field{Expr: &influxql.VarRef{Val: "field1"}},
@ -75,7 +75,9 @@ func TestParser_ParseStatement(t *testing.T) {
&influxql.Dimension{Expr: &influxql.DurationLiteral{Val: 10 * time.Hour}},
},
Limit: 20,
Ascending: true,
SortFields: influxql.SortFields{
&influxql.SortField{Ascending: true},
},
},
},
@ -118,6 +120,21 @@ func TestParser_ParseStatement(t *testing.T) {
},
},
// SELECT statement with multiple ORDER BY fields
{
s: `SELECT field1 FROM myseries ORDER BY ASC, field1, field2 DESC LIMIT 10`,
stmt: &influxql.SelectStatement{
Fields: influxql.Fields{&influxql.Field{Expr: &influxql.VarRef{Val: "field1"}}},
Source: &influxql.Series{Name: "myseries"},
SortFields: influxql.SortFields{
&influxql.SortField{Ascending: true,},
&influxql.SortField{Name: "field1",},
&influxql.SortField{Name: "field2",},
},
Limit: 10,
},
},
// DELETE statement
{
s: `DELETE FROM myseries WHERE host = 'hosta.influxdb.org'`,
@ -137,15 +154,20 @@ func TestParser_ParseStatement(t *testing.T) {
stmt: &influxql.ListSeriesStatement{},
},
// LIST SERIES WHERE statement
// LIST SERIES WHERE with ORDER BY and LIMIT
{
s: `LIST SERIES WHERE region = 'uswest' LIMIT 10`,
s: `LIST SERIES WHERE region = 'uswest' ORDER BY ASC, field1, field2 DESC LIMIT 10`,
stmt: &influxql.ListSeriesStatement{
Condition: &influxql.BinaryExpr{
Op: influxql.EQ,
LHS: &influxql.VarRef{Val: "region"},
RHS: &influxql.StringLiteral{Val: "uswest"},
},
SortFields: influxql.SortFields{
&influxql.SortField{Ascending: true,},
&influxql.SortField{Name: "field1",},
&influxql.SortField{Name: "field2",},
},
Limit: 10,
},
},
@ -185,16 +207,17 @@ func TestParser_ParseStatement(t *testing.T) {
{s: ``, err: `found EOF, expected SELECT at line 1, char 1`},
{s: `SELECT`, err: `found EOF, expected identifier, string, number, bool at line 1, char 8`},
{s: `blah blah`, err: `found blah, expected SELECT at line 1, char 1`},
{s: `SELECT field X`, err: `found X, expected FROM at line 1, char 14`},
{s: `SELECT field FROM "series" WHERE X +;`, err: `found ;, expected identifier, string, number, bool at line 1, char 37`},
{s: `SELECT field FROM myseries GROUP`, err: `found EOF, expected BY at line 1, char 34`},
{s: `SELECT field FROM myseries LIMIT`, err: `found EOF, expected number at line 1, char 34`},
{s: `SELECT field FROM myseries LIMIT 10.5`, err: `fractional parts not allowed in limit at line 1, char 34`},
{s: `SELECT field FROM myseries ORDER`, err: `found EOF, expected BY at line 1, char 34`},
{s: `SELECT field FROM myseries ORDER BY /`, err: `found /, expected ASC, DESC at line 1, char 37`},
{s: `SELECT field AS`, err: `found EOF, expected identifier, string at line 1, char 17`},
{s: `SELECT field FROM 12`, err: `found 12, expected identifier, string at line 1, char 19`},
{s: `SELECT field FROM myseries GROUP BY *`, err: `found *, expected identifier, string, number, bool at line 1, char 37`},
{s: `SELECT field1 X`, err: `found X, expected FROM at line 1, char 15`},
{s: `SELECT field1 FROM "series" WHERE X +;`, err: `found ;, expected identifier, string, number, bool at line 1, char 38`},
{s: `SELECT field1 FROM myseries GROUP`, err: `found EOF, expected BY at line 1, char 35`},
{s: `SELECT field1 FROM myseries LIMIT`, err: `found EOF, expected number at line 1, char 35`},
{s: `SELECT field1 FROM myseries LIMIT 10.5`, err: `fractional parts not allowed in limit at line 1, char 35`},
{s: `SELECT field1 FROM myseries ORDER`, err: `found EOF, expected BY at line 1, char 35`},
{s: `SELECT field1 FROM myseries ORDER BY /`, err: `found /, expected identifier, ASC, or DESC at line 1, char 38`},
{s: `SELECT field1 FROM myseries ORDER BY 1`, err: `found 1, expected identifier, ASC, or DESC at line 1, char 38`},
{s: `SELECT field1 AS`, err: `found EOF, expected identifier, string at line 1, char 18`},
{s: `SELECT field1 FROM 12`, err: `found 12, expected identifier, string at line 1, char 20`},
{s: `SELECT field1 FROM myseries GROUP BY *`, err: `found *, expected identifier, string, number, bool at line 1, char 38`},
{s: `SELECT 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 FROM myseries`, err: `unable to parse number at line 1, char 8`},
{s: `SELECT 10.5h FROM myseries`, err: `found h, expected FROM at line 1, char 12`},
{s: `DELETE`, err: `found EOF, expected FROM at line 1, char 8`},

View File

@ -190,150 +190,4 @@ func TestScanner_Scan_Multi(t *testing.T) {
t.Fatalf("%d. token mismatch:\n\nexp=%#v\n\ngot=%#v", i, exp[i], act[i])
}
}
}
// Test LIST MEASUREMENTS WHERE
func TestScanner_Scan_LIST_MEASUREMENTS_WHERE(t *testing.T) {
type result struct {
tok influxql.Token
pos influxql.Pos
lit string
}
exp := []result{
{tok: influxql.LIST, pos: influxql.Pos{Line: 0, Char: 0}, lit: ""},
{tok: influxql.WS, pos: influxql.Pos{Line: 0, Char: 4}, lit: " "},
{tok: influxql.MEASUREMENTS, pos: influxql.Pos{Line: 0, Char: 5}, lit: ""},
{tok: influxql.WS, pos: influxql.Pos{Line: 0, Char: 17}, lit: " "},
{tok: influxql.WHERE, pos: influxql.Pos{Line: 0, Char: 18}, lit: ""},
{tok: influxql.WS, pos: influxql.Pos{Line: 0, Char: 23}, lit: " "},
{tok: influxql.IDENT, pos: influxql.Pos{Line: 0, Char: 24}, lit: "service"},
{tok: influxql.WS, pos: influxql.Pos{Line: 0, Char: 31}, lit: " "},
{tok: influxql.EQ, pos: influxql.Pos{Line: 0, Char: 32}, lit: ""},
{tok: influxql.WS, pos: influxql.Pos{Line: 0, Char: 33}, lit: " "},
{tok: influxql.STRING, pos: influxql.Pos{Line: 0, Char: 34}, lit: "redis"},
{tok: influxql.EOF, pos: influxql.Pos{Line: 0, Char: 41}, lit: ""},
}
// Create a scanner.
v := `LIST MEASUREMENTS WHERE service = 'redis'`
s := influxql.NewScanner(strings.NewReader(v))
// Continually scan until we reach the end.
var act []result
for {
tok, pos, lit := s.Scan()
act = append(act, result{tok, pos, lit})
if tok == influxql.EOF {
break
}
}
// Verify the token counts match.
if len(exp) != len(act) {
t.Fatalf("token count mismatch: exp=%d, got=%d", len(exp), len(act))
}
// Verify each token matches.
for i := range exp {
if !reflect.DeepEqual(exp[i], act[i]) {
t.Fatalf("%d. token mismatch:\n\nexp=%#v\n\ngot=%#v", i, exp[i], act[i])
}
}
}
// Test LIST SERIES WHERE
func TestScanner_Scan_LIST_SERIES_WHERE(t *testing.T) {
type result struct {
tok influxql.Token
pos influxql.Pos
lit string
}
exp := []result{
{tok: influxql.LIST, pos: influxql.Pos{Line: 0, Char: 0}, lit: ""},
{tok: influxql.WS, pos: influxql.Pos{Line: 0, Char: 4}, lit: " "},
{tok: influxql.SERIES, pos: influxql.Pos{Line: 0, Char: 5}, lit: ""},
{tok: influxql.WS, pos: influxql.Pos{Line: 0, Char: 11}, lit: " "},
{tok: influxql.WHERE, pos: influxql.Pos{Line: 0, Char: 12}, lit: ""},
{tok: influxql.WS, pos: influxql.Pos{Line: 0, Char: 17}, lit: " "},
{tok: influxql.IDENT, pos: influxql.Pos{Line: 0, Char: 18}, lit: "service42"},
{tok: influxql.EQ, pos: influxql.Pos{Line: 0, Char: 27}, lit: ""},
{tok: influxql.STRING, pos: influxql.Pos{Line: 0, Char: 28}, lit: "redis"},
{tok: influxql.EOF, pos: influxql.Pos{Line: 0, Char: 35}, lit: ""},
}
// Create a scanner.
v := `LIST SERIES WHERE service42="redis"`
s := influxql.NewScanner(strings.NewReader(v))
// Continually scan until we reach the end.
var act []result
for {
tok, pos, lit := s.Scan()
act = append(act, result{tok, pos, lit})
if tok == influxql.EOF {
break
}
}
// Verify the token counts match.
if len(exp) != len(act) {
t.Fatalf("token count mismatch: exp=%d, got=%d", len(exp), len(act))
}
// Verify each token matches.
for i := range exp {
if !reflect.DeepEqual(exp[i], act[i]) {
t.Fatalf("%d. token mismatch:\n\nexp=%#v\n\ngot=%#v", i, exp[i], act[i])
}
}
}
// Test LIST TAG KEYS FROM
func TestScanner_Scan_LIST_TAG_KEYS_FROM(t *testing.T) {
type result struct {
tok influxql.Token
pos influxql.Pos
lit string
}
exp := []result{
{tok: influxql.LIST, pos: influxql.Pos{Line: 0, Char: 0}, lit: ""},
{tok: influxql.WS, pos: influxql.Pos{Line: 0, Char: 4}, lit: " "},
{tok: influxql.TAG, pos: influxql.Pos{Line: 0, Char: 5}, lit: ""},
{tok: influxql.WS, pos: influxql.Pos{Line: 0, Char: 8}, lit: " "},
{tok: influxql.KEYS, pos: influxql.Pos{Line: 0, Char: 9}, lit: ""},
{tok: influxql.WS, pos: influxql.Pos{Line: 0, Char: 13}, lit: " "},
{tok: influxql.FROM, pos: influxql.Pos{Line: 0, Char: 14}, lit: ""},
{tok: influxql.WS, pos: influxql.Pos{Line: 0, Char: 18}, lit: " "},
{tok: influxql.IDENT, pos: influxql.Pos{Line: 0, Char: 19}, lit: "temperature"},
{tok: influxql.COMMA, pos: influxql.Pos{Line: 0, Char: 30}, lit: ""},
{tok: influxql.WS, pos: influxql.Pos{Line: 0, Char: 31}, lit: " "},
{tok: influxql.IDENT, pos: influxql.Pos{Line: 0, Char: 32}, lit: "wind_speed"},
{tok: influxql.EOF, pos: influxql.Pos{Line: 0, Char: 43}, lit: ""},
}
// Create a scanner.
v := `LIST TAG KEYS FROM temperature, wind_speed`
s := influxql.NewScanner(strings.NewReader(v))
// Continually scan until we reach the end.
var act []result
for {
tok, pos, lit := s.Scan()
act = append(act, result{tok, pos, lit})
if tok == influxql.EOF {
break
}
}
// Verify the token counts match.
if len(exp) != len(act) {
t.Fatalf("token count mismatch: exp=%d, got=%d", len(exp), len(act))
}
// Verify each token matches.
for i := range exp {
if !reflect.DeepEqual(exp[i], act[i]) {
t.Fatalf("%d. token mismatch:\n\nexp=%#v\n\ngot=%#v", i, exp[i], act[i])
}
}
}
}