diff --git a/influxql/INFLUXQL.md b/influxql/INFLUXQL.md new file mode 100644 index 0000000000..d4d5db9e8b --- /dev/null +++ b/influxql/INFLUXQL.md @@ -0,0 +1,291 @@ +# The Influx Query Language Specification + +## Introduction + +This is a reference for the Influx Query Language ("InfluxQL"). + +InfluxQL is a SQL-like query language for interacting with InfluxDB. It was lovingly crafted to feel familiar to those coming from other +SQL or SQL-like environments while providing features specific to storing +and analyzing time series data. + +## Notation + +This specification uses the same notation used by Google's Go programming language, which can be found at http://golang.org. The syntax is specified in Extended Backus-Naur Form ("EBNF"): + +``` +Production = production_name "=" [ Expression ] "." . +Expression = Alternative { "|" Alternative } . +Alternative = Term { Term } . +Term = production_name | token [ "…" token ] | Group | Option | Repetition . +Group = "(" Expression ")" . +Option = "[" Expression "]" . +Repetition = "{" Expression "}" . +``` + +Notation operators in order of increasing precedence: + +``` +| alternation +() grouping +[] option (0 or 1 times) +{} repetition (0 to n times) +``` + +## Characters & Digits + +``` +newline = /* the Unicode code point U+000A */ . +unicode_char = /* an arbitrary Unicode code point except newline */ . +ascii_letter = "A" .. "Z" | "a" .. "z" . +decimal_digit = "0" .. "9" . +``` + +## Database name + +Database names are more limited than other identifiers because they appear in URLs. + +``` +db_name = ascii_letter { ascii_letter | decimal_digit | "_" | "-" } . +``` + +## Identifiers + +``` +identifier = unquoted_identifier | quoted_identifier . +unquoted_identifier = ascii_letter { ascii_letter | decimal_digit | "_" | "." } . +quoted_identifier = `"` unicode_char { unicode_char } `"` . +``` + +## Keywords + +``` +ALL ALTER AS ASC BEGIN +BY CREATE CONTINUOUS DATABASE DEFAULT +DELETE DESC DROP DURATION END +EXISTS EXPLAIN FIELD FROM GRANT +GROUP IF INNER INSERT INTO +KEYS LIMIT LIST MEASUREMENT MEASUREMENTS +ON ORDER PASSWORD POLICY PRIVILEGES +QUERIES QUERY READ REPLICATION RETENTION +EVOKE SELECT SERIES TAG TO +USER VALUES WHERE WITH WRITE +``` + +## Literals + +### Numbers + +``` +int_lit = decimal_lit . +decimal_lit = ( "1" .. "9" ) { decimal_digit } . +float_lit = decimals "." decimals . +decimals = decimal_digit { decimal_digit } . +``` + +### Strings + +``` +string_lit = '"' { unicode_char } '"' . +``` + +### Durations + +``` +duration_lit = decimals duration_unit . +duration_unit = "u" | "µ" | "s" | "h" | "d" | "w" | "ms" . +``` + +## Queries + +A query is composed of one or more statements separated by a semicolon. + +``` +query = statement { ; statement } . + +statement = alter_retention_policy_stmt | + create_continuous_query_stmt | + create_database_stmt | + create_retention_policy_stmt | + create_user_stmt | + delete_stmt | + drop_continuous_query_stmt | + drop_database_stmt | + drop_series_stmt | + drop_user_stmt | + grant_stmt | + list_continuous_queries_stmt | + list_databases_stmt | + list_field_key_stmt | + list_field_value_stmt | + list_measurements_stmt | + list_series_stmt | + list_tag_key_stmt | + list_tag_value_stmt | + revoke_stmt | + select_stmt . +``` + +## Statements + +### ALTER RETENTION POLICY + +``` +alter_retention_policy_stmt = "ALTER RETENTION POLICY" policy_name "ON" + db_name retention_policy_option + [ retention_policy_option ] + [ retention_policy_option ] . + +policy_name = identifier . + +retention_policy_option = retention_policy_duration | + retention_policy_replication | + "DEFAULT" . + +retention_policy_duration = "DURATION" duration_lit . +retention_policy_replication = "REPLICATION" int_lit +``` + +#### Examples: + +```sql +-- Set default retention policy for mydb to 1h.cpu. +ALTER RETENTION POLICY "1h.cpu" ON mydb DEFAULT; + +-- Change duration and replication factor. +ALTER RETENTION POLICY policy1 ON somedb DURATION 1h REPLICATION 4 +``` + +### CREATE CONTINUOUS QUERY + +``` +create_continuous_query_stmt = "CREATE CONTINUOUS QUERY" query_name "ON" db_name + "BEGIN" select_stmt "END" . + +query_name = identifier . +``` + +#### Examples: + +```sql +CREATE CONTINUOUS QUERY 10m_event_count +ON db_name +BEGIN + SELECT count(value) + INTO 10m.events + FROM events + GROUP BY time(10m) +END; + +-- this selects from the output of one continuous query and outputs to another series +CREATE CONTINUOUS QUERY 1h_event_count +ON db_name +BEGIN + SELECT sum(count) as count + INTO 1h.events + FROM events + GROUP BY time(1h) +END; +``` + +### CREATE DATABASE + +``` +create_database_stmt = "CREATE DATABASE" db_name +``` + +#### Example: + +```sql +CREATE DATABASE foo +``` + +### CREATE RETENTION POLICY + +``` +create_retention_policy_stmt = "CREATE RETENTION POLICY" policy_name "ON" + db_name retention_policy_duration + retention_policy_replication + [ "DEFAULT" ] . +``` + +#### Examples + +```sql +-- Create a retention policy. +CREATE RETENTION POLICY "10m.events" ON somedb DURATION 10m REPLICATION 2; + +-- Create a retention policy and set it as the default. +CREATE RETENTION POLICY "10m.events" ON somedb DURATION 10m REPLICATION 2 DEFAULT; +``` + +### CREATE USER + +``` +create_user_stmt = "CREATE USER" user_name "WITH PASSWORD" password + [ "WITH ALL PRIVILEGES" ] . +``` + +#### Examples: + +```sql +-- Create a normal database user. +CREATE USER jdoe WITH PASSWORD "1337password"; + +-- Create a cluster admin. +-- Note: Unlike the GRANT statement, the "PRIVILEGES" keyword is required here. +CREATE USER jdoe WITH PASSWORD "1337password" WITH ALL PRIVILEGES; +``` + +### DELETE + +``` +delete_stmt = "DELETE" from_clause where_clause . +``` + +#### Example: + +```sql +DELETE FROM cpu WHERE region = 'uswest' +``` + +### GRANT + +``` +grant_stmt = "GRANT" privilege [ on_clause ] to_clause +``` + +#### Examples: + +```sql +-- grant cluster admin privileges +GRANT ALL TO jdoe; + +-- grant read access to a database +GRANT READ ON mydb TO jdoe; +``` + +## Clauses + +``` +from_clause = "FROM" measurements . + +where_clause = "WHERE" expr . + +on_clause = db_name . + +to_clause = user_name . +``` + +## Other + +``` +expr = + +measurements = + +user_name = identifier . + +password = identifier . + +privilege = "ALL" [ "PRIVILEGES" ] | "READ" | "WRITE" . +``` diff --git a/influxql/ast.go b/influxql/ast.go index 5ce7cde859..92bbbd421f 100644 --- a/influxql/ast.go +++ b/influxql/ast.go @@ -47,47 +47,49 @@ type Node interface { func (_ *Query) node() {} func (_ Statements) node() {} -func (_ *SelectStatement) node() {} -func (_ *DeleteStatement) node() {} -func (_ *ListSeriesStatement) node() {} -func (_ *ListMeasurementsStatement) node() {} -func (_ *ListTagKeysStatement) node() {} -func (_ *ListTagValuesStatement) node() {} -func (_ *ListFieldKeysStatement) node() {} -func (_ *ListFieldValuesStatement) node() {} -func (_ *ListContinuousQueriesStatement) node() {} -func (_ *DropSeriesStatement) node() {} -func (_ *DropContinuousQueryStatement) node() {} -func (_ *DropDatabaseStatement) node() {} -func (_ *DropUserStatement) node() {} +func (_ *AlterRetentionPolicyStatement) node() {} func (_ *CreateContinuousQueryStatement) node() {} func (_ *CreateDatabaseStatement) node() {} -func (_ *CreateUserStatement) node() {} func (_ *CreateRetentionPolicyStatement) node() {} +func (_ *CreateUserStatement) node() {} +func (_ *DeleteStatement) node() {} +func (_ *DropContinuousQueryStatement) node() {} +func (_ *DropDatabaseStatement) node() {} +func (_ *DropSeriesStatement) node() {} +func (_ *DropUserStatement) node() {} func (_ *GrantStatement) node() {} +func (_ *ListContinuousQueriesStatement) node() {} +func (_ *ListDatabasesStatement) node() {} +func (_ *ListFieldKeysStatement) node() {} +func (_ *ListFieldValuesStatement) node() {} +func (_ *ListMeasurementsStatement) node() {} +func (_ *ListSeriesStatement) node() {} +func (_ *ListTagKeysStatement) node() {} +func (_ *ListTagValuesStatement) node() {} func (_ *RevokeStatement) node() {} -func (_ *AlterRetentionPolicyStatement) node() {} +func (_ *SelectStatement) node() {} -func (_ Fields) node() {} -func (_ *Field) node() {} -func (_ Dimensions) node() {} +func (_ *BinaryExpr) node() {} +func (_ *BooleanLiteral) node() {} +func (_ *Call) node() {} func (_ *Dimension) node() {} +func (_ Dimensions) node() {} +func (_ *DurationLiteral) node() {} +func (_ *Field) node() {} +func (_ Fields) node() {} +func (_ *Join) node() {} func (_ *Measurement) node() {} func (_ Measurements) node() {} -func (_ *Join) node() {} func (_ *Merge) node() {} -func (_ *VarRef) node() {} -func (_ *Call) node() {} func (_ *NumberLiteral) node() {} -func (_ *StringLiteral) node() {} -func (_ *BooleanLiteral) node() {} -func (_ *TimeLiteral) node() {} -func (_ *DurationLiteral) node() {} -func (_ *BinaryExpr) node() {} func (_ *ParenExpr) node() {} -func (_ *Wildcard) node() {} -func (_ SortFields) node() {} func (_ *SortField) node() {} +func (_ SortFields) node() {} +func (_ *StringLiteral) node() {} +func (_ *Target) node() {} +func (_ *TimeLiteral) node() {} +func (_ *VarRef) node() {} +func (_ *Wildcard) node() {} // Query represents a collection of ordered statements. type Query struct { @@ -115,26 +117,27 @@ type Statement interface { stmt() } -func (_ *SelectStatement) stmt() {} -func (_ *DeleteStatement) stmt() {} -func (_ *ListSeriesStatement) stmt() {} -func (_ *DropSeriesStatement) stmt() {} -func (_ *ListContinuousQueriesStatement) stmt() {} +func (_ *AlterRetentionPolicyStatement) stmt() {} func (_ *CreateContinuousQueryStatement) stmt() {} +func (_ *CreateDatabaseStatement) stmt() {} +func (_ *CreateRetentionPolicyStatement) stmt() {} +func (_ *CreateUserStatement) stmt() {} +func (_ *DeleteStatement) stmt() {} func (_ *DropContinuousQueryStatement) stmt() {} -func (_ *ListMeasurementsStatement) stmt() {} -func (_ *ListTagKeysStatement) stmt() {} -func (_ *ListTagValuesStatement) stmt() {} +func (_ *DropDatabaseStatement) stmt() {} +func (_ *DropSeriesStatement) stmt() {} +func (_ *DropUserStatement) stmt() {} +func (_ *GrantStatement) stmt() {} +func (_ *ListContinuousQueriesStatement) stmt() {} +func (_ *ListDatabasesStatement) stmt() {} func (_ *ListFieldKeysStatement) stmt() {} func (_ *ListFieldValuesStatement) stmt() {} -func (_ *CreateDatabaseStatement) stmt() {} -func (_ *CreateUserStatement) stmt() {} -func (_ *GrantStatement) stmt() {} +func (_ *ListMeasurementsStatement) stmt() {} +func (_ *ListSeriesStatement) stmt() {} +func (_ *ListTagKeysStatement) stmt() {} +func (_ *ListTagValuesStatement) stmt() {} func (_ *RevokeStatement) stmt() {} -func (_ *CreateRetentionPolicyStatement) stmt() {} -func (_ *DropDatabaseStatement) stmt() {} -func (_ *DropUserStatement) stmt() {} -func (_ *AlterRetentionPolicyStatement) stmt() {} +func (_ *SelectStatement) stmt() {} // Expr represents an expression that can be evaluated to a value. type Expr interface { @@ -142,15 +145,15 @@ type Expr interface { expr() } -func (_ *VarRef) expr() {} -func (_ *Call) expr() {} -func (_ *NumberLiteral) expr() {} -func (_ *StringLiteral) expr() {} -func (_ *BooleanLiteral) expr() {} -func (_ *TimeLiteral) expr() {} -func (_ *DurationLiteral) expr() {} func (_ *BinaryExpr) expr() {} +func (_ *BooleanLiteral) expr() {} +func (_ *Call) expr() {} +func (_ *DurationLiteral) expr() {} +func (_ *NumberLiteral) expr() {} func (_ *ParenExpr) expr() {} +func (_ *StringLiteral) expr() {} +func (_ *TimeLiteral) expr() {} +func (_ *VarRef) expr() {} func (_ *Wildcard) expr() {} // Source represents a source of data for a statement. @@ -159,8 +162,8 @@ type Source interface { source() } -func (_ *Measurement) source() {} func (_ *Join) source() {} +func (_ *Measurement) source() {} func (_ *Merge) source() {} // SortField represens a field to sort results by. @@ -228,6 +231,9 @@ type CreateUserStatement struct { // User's password Password string + + // User's privilege level. + Privilege *Privilege } // String returns a string representation of the create user statement. @@ -237,6 +243,12 @@ func (s *CreateUserStatement) String() string { _, _ = buf.WriteString(s.Name) _, _ = buf.WriteString(" WITH PASSWORD ") _, _ = buf.WriteString(s.Password) + + if s.Privilege != nil { + _, _ = buf.WriteString(" WITH ") + _, _ = buf.WriteString(s.Privilege.String()) + } + return buf.String() } @@ -263,6 +275,9 @@ const ( AllPrivileges ) +// NewPrivilege returns an initialized *Privilege. +func NewPrivilege(p Privilege) *Privilege { return &p } + // String returns a string representation of a Privilege. func (p Privilege) String() string { switch p { @@ -334,7 +349,7 @@ type CreateRetentionPolicyStatement struct { Name string // Name of database this policy belongs to. - DB string + Database string // Duration data written to this policy will be retained. Duration time.Duration @@ -352,7 +367,7 @@ func (s *CreateRetentionPolicyStatement) String() string { _, _ = buf.WriteString("CREATE RETENTION POLICY ") _, _ = buf.WriteString(s.Name) _, _ = buf.WriteString(" ON ") - _, _ = buf.WriteString(s.DB) + _, _ = buf.WriteString(s.Database) _, _ = buf.WriteString(" DURATION ") _, _ = buf.WriteString(FormatDuration(s.Duration)) _, _ = buf.WriteString(" REPLICATION ") @@ -369,7 +384,7 @@ type AlterRetentionPolicyStatement struct { Name string // Name of the database this policy belongs to. - DB string + Database string // Duration data written to this policy will be retained. Duration *time.Duration @@ -387,7 +402,7 @@ func (s *AlterRetentionPolicyStatement) String() string { _, _ = buf.WriteString("ALTER RETENTION POLICY ") _, _ = buf.WriteString(s.Name) _, _ = buf.WriteString(" ON ") - _, _ = buf.WriteString(s.DB) + _, _ = buf.WriteString(s.Database) if s.Duration != nil { _, _ = buf.WriteString(" DURATION ") @@ -411,6 +426,9 @@ type SelectStatement struct { // Expressions returned from the selection. Fields Fields + // Target (destination) for the result of the select. + Target *Target + // Expressions used for grouping the selection. Dimensions Dimensions @@ -433,6 +451,11 @@ func (s *SelectStatement) String() string { var buf bytes.Buffer _, _ = buf.WriteString("SELECT ") _, _ = buf.WriteString(s.Fields.String()) + + if s.Target != nil { + _, _ = buf.WriteString(" ") + _, _ = buf.WriteString(s.Target.String()) + } _, _ = buf.WriteString(" FROM ") _, _ = buf.WriteString(s.Source.String()) if s.Condition != nil { @@ -591,6 +614,38 @@ func MatchSource(src Source, name string) string { return "" } +// Target represents a target (destination) policy, measurment, and DB. +type Target struct { + // Retention policy to write into. + RetentionPolicy string + + // Measurement to write into. + Measurement string + + // Database to write into. + Database string +} + +// String returns a string representation of the Target. +func (t *Target) String() string { + var buf bytes.Buffer + _, _ = buf.WriteString("INTO ") + + if t.RetentionPolicy != "" { + _, _ = buf.WriteString(t.RetentionPolicy) + _, _ = buf.WriteString(".") + } + + _, _ = buf.WriteString(t.Measurement) + + if t.Database != "" { + _, _ = buf.WriteString(" ON ") + _, _ = buf.WriteString(t.Database) + } + + return buf.String() +} + // DeleteStatement represents a command for removing data from the database. type DeleteStatement struct { // Data source that values are removed from. @@ -659,16 +714,27 @@ type ListContinuousQueriesStatement struct{} // String returns a string representation of the list continuous queries statement. func (s *ListContinuousQueriesStatement) String() string { return "LIST CONTINUOUS QUERIES" } +// ListDatabasesStatement represents a command for listing all databases in the cluster. +type ListDatabasesStatement struct{} + +// String returns a string representation of the list databases command. +func (s *ListDatabasesStatement) String() string { return "LIST DATABASES" } + // CreateContinuousQueriesStatement represents a command for creating a continuous query. type CreateContinuousQueryStatement struct { - Name string + // Name of the continuous query to be created. + Name string + + // Name of the database to create the continuous query on. + Database string + + // Source of data (SELECT statement). Source *SelectStatement - Target string } // String returns a string representation of the statement. func (s *CreateContinuousQueryStatement) String() string { - return fmt.Sprintf("CREATE CONTINUOUS QUERY %s AS %s INTO %s", s.Name, s.Source.String(), s.Target) + return fmt.Sprintf("CREATE CONTINUOUS QUERY %s ON %s BEGIN %s END", s.Name, s.Database, s.Source.String()) } // DropContinuousQueriesStatement represents a command for removing a continuous query. diff --git a/influxql/parser.go b/influxql/parser.go index 3949003cfb..5adfac1852 100644 --- a/influxql/parser.go +++ b/influxql/parser.go @@ -65,7 +65,7 @@ func (p *Parser) ParseStatement() (Statement, error) { tok, pos, lit := p.scanIgnoreWhitespace() switch tok { case SELECT: - return p.parseSelectStatement() + return p.parseSelectStatement(targetNotRequired) case DELETE: return p.parseDeleteStatement() case LIST: @@ -93,6 +93,8 @@ func (p *Parser) parseListStatement() (Statement, error) { return p.parseListSeriesStatement() } else if tok == CONTINUOUS { return p.parseListContinuousQueriesStatement() + } else if tok == DATABASES { + return p.parseListDatabasesStatement() } else if tok == MEASUREMENTS { return p.parseListMeasurementsStatement() } else if tok == TAG { @@ -190,7 +192,7 @@ func (p *Parser) parseCreateRetentionPolicyStatement() (*CreateRetentionPolicySt if err != nil { return nil, err } - stmt.DB = ident + stmt.Database = ident // Parse required DURATION token. tok, pos, lit := p.scanIgnoreWhitespace() @@ -249,9 +251,9 @@ func (p *Parser) parseAlterRetentionPolicyStatement() (*AlterRetentionPolicyStat if err != nil { return nil, err } - stmt.DB = ident + stmt.Database = ident - // Loop through option tokens (DURATION, RETENTION, DEFAULT, etc.). + // Loop through option tokens (DURATION, REPLICATION, DEFAULT, etc.). maxNumOptions := 3 Loop: for i := 0; i < maxNumOptions; i++ { @@ -442,7 +444,7 @@ func (p *Parser) parsePrivilege() (Privilege, error) { // parseSelectStatement parses a select string and returns a Statement AST object. // This function assumes the SELECT token has already been consumed. -func (p *Parser) parseSelectStatement() (*SelectStatement, error) { +func (p *Parser) parseSelectStatement(tr targetRequirement) (*SelectStatement, error) { stmt := &SelectStatement{} // Parse fields: "SELECT FIELD+". @@ -452,6 +454,14 @@ func (p *Parser) parseSelectStatement() (*SelectStatement, error) { } stmt.Fields = fields + // Parse target: "INTO" + target, err := p.parseTarget(tr) + if err != nil { + return nil, err + } else if target != nil { + stmt.Target = target + } + // Parse source. if tok, pos, lit := p.scanIgnoreWhitespace(); tok != FROM { return nil, newParseError(tokstr(tok, lit), []string{"FROM"}, pos) @@ -493,6 +503,63 @@ func (p *Parser) parseSelectStatement() (*SelectStatement, error) { return stmt, nil } +// targetRequirement specifies whether or not a target clause is required. +type targetRequirement int + +const ( + targetRequired targetRequirement = iota + targetNotRequired +) + +// parseTarget parses a string and returns a Target. +func (p *Parser) parseTarget(tr targetRequirement) (*Target, error) { + if tok, pos, lit := p.scanIgnoreWhitespace(); tok != INTO { + if tr == targetRequired { + return nil, newParseError(tokstr(tok, lit), []string{"INTO"}, pos) + } + p.unscan() + return nil, nil + } + + // Parse identifier. Could be policy or measurement name. + ident, err := p.parseIdentifier() + if err != nil { + return nil, err + } + + target := &Target{} + + tok, _, _ := p.scanIgnoreWhitespace() + if tok == DOT { + // Previous identifier was retention policy name. + target.RetentionPolicy = ident + + // Parse required measurement. + ident, err = p.parseIdentifier() + if err != nil { + return nil, err + } + } else { + p.unscan() + } + + target.Measurement = ident + + // Parse optional ON. + if tok, _, _ := p.scanIgnoreWhitespace(); tok != ON { + p.unscan() + return target, nil + } + + // Found an ON token so parse required identifier. + if ident, err = p.parseIdentifier(); err != nil { + return nil, err + } + target.Database = ident + + return target, nil +} + // parseDeleteStatement parses a delete string and returns a DeleteStatement. // This function assumes the DELETE token has already been consumed. func (p *Parser) parseDeleteStatement() (*DeleteStatement, error) { @@ -760,6 +827,13 @@ func (p *Parser) parseListContinuousQueriesStatement() (*ListContinuousQueriesSt return stmt, nil } +// parseListDatabasesStatement parses a string and returns a ListDatabasesStatement. +// This function assumes the "LIST DATABASE" tokens have already been consumed. +func (p *Parser) parseListDatabasesStatement() (*ListDatabasesStatement, error) { + stmt := &ListDatabasesStatement{} + return stmt, nil +} + // parseCreateContinuousQueriesStatement parses a string and returns a CreateContinuousQueryStatement. // This function assumes the "CREATE CONTINUOUS" tokens have already been consumed. func (p *Parser) parseCreateContinuousQueryStatement() (*CreateContinuousQueryStatement, error) { @@ -771,39 +845,40 @@ func (p *Parser) parseCreateContinuousQueryStatement() (*CreateContinuousQuerySt } // Read the id of the query to create. - tok, pos, lit := p.scanIgnoreWhitespace() - if tok != IDENT && tok != STRING { - return nil, newParseError(tokstr(tok, lit), []string{"identifier", "string"}, pos) + ident, err := p.parseIdentifier() + if err != nil { + return nil, err } - stmt.Name = lit + stmt.Name = ident - // Expect an "AS SELECT" keyword. - if tok, pos, lit := p.scanIgnoreWhitespace(); tok != AS { - return nil, newParseError(tokstr(tok, lit), []string{"AS"}, pos) + // Expect an "ON" keyword. + if tok, pos, lit := p.scanIgnoreWhitespace(); tok != ON { + return nil, newParseError(tokstr(tok, lit), []string{"ON"}, pos) } - if tok, pos, lit := p.scanIgnoreWhitespace(); tok != SELECT { - return nil, newParseError(tokstr(tok, lit), []string{"SELECT"}, pos) + + // Read the name of the database to create the query on. + if ident, err = p.parseIdentifier(); err != nil { + return nil, err + } + stmt.Database = ident + + // Expect a "BEGIN SELECT" tokens. + if err := p.parseTokens([]Token{BEGIN, SELECT}); err != nil { + return nil, err } // Read the select statement to be used as the source. - source, err := p.parseSelectStatement() + source, err := p.parseSelectStatement(targetRequired) if err != nil { return nil, err } stmt.Source = source - // Expect an INTO keyword. - if tok, pos, lit := p.scanIgnoreWhitespace(); tok != INTO { - return nil, newParseError(tokstr(tok, lit), []string{"INTO"}, pos) + // Expect a "END" keyword. + if tok, pos, lit := p.scanIgnoreWhitespace(); tok != END { + return nil, newParseError(tokstr(tok, lit), []string{"END"}, pos) } - // Read the target of the query. - tok, pos, lit = p.scanIgnoreWhitespace() - if tok != IDENT && tok != STRING { - return nil, newParseError(tokstr(tok, lit), []string{"identifier", "string"}, pos) - } - stmt.Target = lit - return stmt, nil } @@ -843,11 +918,11 @@ func (p *Parser) parseCreateUserStatement() (*CreateUserStatement, error) { stmt := &CreateUserStatement{} // Parse name of the user to be created. - tok, pos, lit := p.scanIgnoreWhitespace() - if tok != IDENT && tok != STRING { - return nil, newParseError(tokstr(tok, lit), []string{"identifier", "string"}, pos) + ident, err := p.parseIdentifier() + if err != nil { + return nil, err } - stmt.Name = lit + stmt.Name = ident // Consume "WITH PASSWORD" tokens if err := p.parseTokens([]Token{WITH, PASSWORD}); err != nil { @@ -855,11 +930,23 @@ func (p *Parser) parseCreateUserStatement() (*CreateUserStatement, error) { } // Parse new user's password - tok, pos, lit = p.scanIgnoreWhitespace() - if tok != IDENT && tok != STRING { - return nil, newParseError(tokstr(tok, lit), []string{"identifier", "string"}, pos) + if ident, err = p.parseIdentifier(); err != nil { + return nil, err } - stmt.Password = lit + stmt.Password = ident + + // Check for option WITH clause. + if tok, _, _ := p.scanIgnoreWhitespace(); tok != WITH { + p.unscan() + return stmt, nil + } + + // We only allow granting of "ALL PRIVILEGES" during CREATE USER. + // All other privileges must be granted using a GRANT statement. + if err := p.parseTokens([]Token{ALL, PRIVILEGES}); err != nil { + return nil, err + } + stmt.Privilege = NewPrivilege(AllPrivileges) return stmt, nil } diff --git a/influxql/parser_test.go b/influxql/parser_test.go index fbdaeefec3..57d411fc7c 100644 --- a/influxql/parser_test.go +++ b/influxql/parser_test.go @@ -147,6 +147,12 @@ func TestParser_ParseStatement(t *testing.T) { }, }, + // LIST DATABASES + { + s: `LIST DATABASES`, + stmt: &influxql.ListDatabasesStatement{}, + }, + // LIST SERIES statement { s: `LIST SERIES`, @@ -277,16 +283,34 @@ func TestParser_ParseStatement(t *testing.T) { stmt: &influxql.ListContinuousQueriesStatement{}, }, - // CREATE CONTINUOUS QUERY statement + // CREATE CONTINUOUS QUERY ... INTO { - s: `CREATE CONTINUOUS QUERY myquery AS SELECT count() FROM myseries INTO foo`, + s: `CREATE CONTINUOUS QUERY myquery ON testdb BEGIN SELECT count() INTO measure1 FROM myseries END`, stmt: &influxql.CreateContinuousQueryStatement{ - Name: "myquery", + Name: "myquery", + Database: "testdb", Source: &influxql.SelectStatement{ Fields: influxql.Fields{&influxql.Field{Expr: &influxql.Call{Name: "count"}}}, + Target: &influxql.Target{Measurement: "measure1"}, + Source: &influxql.Measurement{Name: "myseries"}, + }, + }, + }, + + // CREATE CONTINUOUS QUERY ... INTO . + { + s: `CREATE CONTINUOUS QUERY myquery ON testdb BEGIN SELECT count() INTO "1h.policy1"."cpu.load" FROM myseries END`, + stmt: &influxql.CreateContinuousQueryStatement{ + Name: "myquery", + Database: "testdb", + Source: &influxql.SelectStatement{ + Fields: influxql.Fields{&influxql.Field{Expr: &influxql.Call{Name: "count"}}}, + Target: &influxql.Target{ + RetentionPolicy: "1h.policy1", + Measurement: "cpu.load", + }, Source: &influxql.Measurement{Name: "myseries"}, }, - Target: "foo", }, }, @@ -307,6 +331,16 @@ func TestParser_ParseStatement(t *testing.T) { }, }, + // CREATE USER ... WITH ALL PRIVILEGES + { + s: `CREATE USER testuser WITH PASSWORD pwd1337 WITH ALL PRIVILEGES`, + stmt: &influxql.CreateUserStatement{ + Name: "testuser", + Password: "pwd1337", + Privilege: influxql.NewPrivilege(influxql.AllPrivileges), + }, + }, + // DROP CONTINUOUS QUERY statement { s: `DROP CONTINUOUS QUERY myquery`, @@ -428,7 +462,7 @@ func TestParser_ParseStatement(t *testing.T) { s: `CREATE RETENTION POLICY policy1 ON testdb DURATION 1h REPLICATION 2`, stmt: &influxql.CreateRetentionPolicyStatement{ Name: "policy1", - DB: "testdb", + Database: "testdb", Duration: time.Hour, Replication: 2, }, @@ -439,7 +473,7 @@ func TestParser_ParseStatement(t *testing.T) { s: `CREATE RETENTION POLICY policy1 ON testdb DURATION 2m REPLICATION 4 DEFAULT`, stmt: &influxql.CreateRetentionPolicyStatement{ Name: "policy1", - DB: "testdb", + Database: "testdb", Duration: 2 * time.Minute, Replication: 4, Default: true, @@ -506,6 +540,10 @@ func TestParser_ParseStatement(t *testing.T) { {s: `DROP DATABASE`, err: `found EOF, expected identifier at line 1, char 15`}, {s: `DROP USER`, err: `found EOF, expected identifier at line 1, char 11`}, {s: `CREATE USER testuser`, err: `found EOF, expected WITH at line 1, char 22`}, + {s: `CREATE USER testuser WITH`, err: `found EOF, expected PASSWORD at line 1, char 27`}, + {s: `CREATE USER testuser WITH PASSWORD`, err: `found EOF, expected identifier at line 1, char 36`}, + {s: `CREATE USER testuser WITH PASSWORD "pwd" WITH`, err: `found EOF, expected ALL at line 1, char 47`}, + {s: `CREATE USER testuser WITH PASSWORD "pwd" WITH ALL`, err: `found EOF, expected PRIVILEGES at line 1, char 51`}, {s: `GRANT`, err: `found EOF, expected READ, WRITE, ALL [PRIVILEGES] at line 1, char 7`}, {s: `GRANT BOGUS`, err: `found BOGUS, expected READ, WRITE, ALL [PRIVILEGES] at line 1, char 7`}, {s: `GRANT READ`, err: `found EOF, expected ON at line 1, char 12`}, @@ -545,6 +583,10 @@ func TestParser_ParseStatement(t *testing.T) { t.Errorf("%d. %q: error mismatch:\n exp=%s\n got=%s\n\n", i, tt.s, tt.err, err) } else if tt.err == "" && !reflect.DeepEqual(tt.stmt, stmt) { t.Errorf("%d. %q\n\nstmt mismatch:\n\nexp=%#v\n\ngot=%#v\n\n", i, tt.s, tt.stmt, stmt) + exp := tt.stmt.(*influxql.CreateContinuousQueryStatement).Source.Target + got := stmt.(*influxql.CreateContinuousQueryStatement).Source.Target + t.Errorf("exp.String() = %#v\n", *exp) + t.Errorf("got.String() = %#v\n", *got) } } } @@ -833,9 +875,9 @@ func errstring(err error) string { // newAlterRetentionPolicyStatement creates an initialized AlterRetentionPolicyStatement. func newAlterRetentionPolicyStatement(name string, DB string, d time.Duration, replication int, dfault bool) *influxql.AlterRetentionPolicyStatement { stmt := &influxql.AlterRetentionPolicyStatement{ - Name: name, - DB: DB, - Default: dfault, + Name: name, + Database: DB, + Default: dfault, } if d > -1 { diff --git a/influxql/scanner.go b/influxql/scanner.go index 64bdb65519..4b8e4d9d95 100644 --- a/influxql/scanner.go +++ b/influxql/scanner.go @@ -42,7 +42,14 @@ func (s *Scanner) Scan() (tok Token, pos Pos, lit string) { return EOF, pos, "" case '"', '\'': return s.scanString() - case '.', '+', '-': + case '.': + ch1, _ := s.r.read() + s.r.unread() + if isDigit(ch1) { + return s.scanNumber() + } + return DOT, pos, "" + case '+', '-': return s.scanNumber() case '*': return MUL, pos, "" @@ -233,7 +240,6 @@ func (s *Scanner) scanNumber() (tok Token, pos Pos, lit string) { } s.r.unread() } - return NUMBER, pos, buf.String() } diff --git a/influxql/scanner_test.go b/influxql/scanner_test.go index 5d65b72619..580aaa5bb8 100644 --- a/influxql/scanner_test.go +++ b/influxql/scanner_test.go @@ -54,6 +54,7 @@ func TestScanner_Scan(t *testing.T) { {s: `)`, tok: influxql.RPAREN}, {s: `,`, tok: influxql.COMMA}, {s: `;`, tok: influxql.SEMICOLON}, + {s: `.`, tok: influxql.DOT}, // Identifiers {s: `foo`, tok: influxql.IDENT, lit: `foo`}, @@ -79,7 +80,7 @@ func TestScanner_Scan(t *testing.T) { {s: `.23`, tok: influxql.NUMBER, lit: `.23`}, {s: `+.23`, tok: influxql.NUMBER, lit: `+.23`}, {s: `-.23`, tok: influxql.NUMBER, lit: `-.23`}, - {s: `.`, tok: influxql.ILLEGAL, lit: `.`}, + //{s: `.`, tok: influxql.ILLEGAL, lit: `.`}, {s: `-.`, tok: influxql.SUB, lit: ``}, {s: `+.`, tok: influxql.ADD, lit: ``}, {s: `10.3s`, tok: influxql.NUMBER, lit: `10.3`}, @@ -100,15 +101,18 @@ func TestScanner_Scan(t *testing.T) { {s: `ALTER`, tok: influxql.ALTER}, {s: `AS`, tok: influxql.AS}, {s: `ASC`, tok: influxql.ASC}, + {s: `BEGIN`, tok: influxql.BEGIN}, {s: `BY`, tok: influxql.BY}, {s: `CREATE`, tok: influxql.CREATE}, {s: `CONTINUOUS`, tok: influxql.CONTINUOUS}, {s: `DATABASE`, tok: influxql.DATABASE}, + {s: `DATABASES`, tok: influxql.DATABASES}, {s: `DEFAULT`, tok: influxql.DEFAULT}, {s: `DELETE`, tok: influxql.DELETE}, {s: `DESC`, tok: influxql.DESC}, {s: `DROP`, tok: influxql.DROP}, {s: `DURATION`, tok: influxql.DURATION}, + {s: `END`, tok: influxql.END}, {s: `EXISTS`, tok: influxql.EXISTS}, {s: `EXPLAIN`, tok: influxql.EXPLAIN}, {s: `FIELD`, tok: influxql.FIELD}, diff --git a/influxql/token.go b/influxql/token.go index bed5b043f7..b07f7e8912 100644 --- a/influxql/token.go +++ b/influxql/token.go @@ -47,6 +47,7 @@ const ( RPAREN // ) COMMA // , SEMICOLON // ; + DOT // . keyword_beg // Keywords @@ -54,15 +55,18 @@ const ( ALTER AS ASC + BEGIN BY CREATE CONTINUOUS DATABASE + DATABASES DEFAULT DELETE DESC DROP DURATION + END EXISTS EXPLAIN FIELD @@ -132,20 +136,24 @@ var tokens = [...]string{ RPAREN: ")", COMMA: ",", SEMICOLON: ";", + DOT: ".", ALL: "ALL", ALTER: "ALTER", AS: "AS", ASC: "ASC", + BEGIN: "BEGIN", BY: "BY", CREATE: "CREATE", CONTINUOUS: "CONTINUOUS", DATABASE: "DATABASE", + DATABASES: "DATABASES", DEFAULT: "DEFAULT", DELETE: "DELETE", DESC: "DESC", DROP: "DROP", DURATION: "DURATION", + END: "END", EXISTS: "EXISTS", EXPLAIN: "EXPLAIN", FIELD: "FIELD",