commit
3065a0358f
|
@ -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" .
|
||||
```
|
182
influxql/ast.go
182
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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 <measurement>
|
||||
{
|
||||
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 <retention-policy>.<measurement>
|
||||
{
|
||||
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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue