package influxql_test import ( "fmt" "reflect" "strings" "testing" "time" "github.com/influxdata/influxdb/influxql" ) // Ensure a value's data type can be retrieved. func TestInspectDataType(t *testing.T) { for i, tt := range []struct { v interface{} typ influxql.DataType }{ {float64(100), influxql.Float}, {int64(100), influxql.Integer}, {int32(100), influxql.Integer}, {100, influxql.Integer}, {true, influxql.Boolean}, {"string", influxql.String}, {time.Now(), influxql.Time}, {time.Second, influxql.Duration}, {nil, influxql.Unknown}, } { if typ := influxql.InspectDataType(tt.v); tt.typ != typ { t.Errorf("%d. %v (%s): unexpected type: %s", i, tt.v, tt.typ, typ) continue } } } func TestDataType_String(t *testing.T) { for i, tt := range []struct { typ influxql.DataType v string }{ {influxql.Float, "float"}, {influxql.Integer, "integer"}, {influxql.Boolean, "boolean"}, {influxql.String, "string"}, {influxql.Time, "time"}, {influxql.Duration, "duration"}, {influxql.Unknown, "unknown"}, } { if v := tt.typ.String(); tt.v != v { t.Errorf("%d. %v (%s): unexpected string: %s", i, tt.typ, tt.v, v) } } } // Ensure the SELECT statement can extract substatements. func TestSelectStatement_Substatement(t *testing.T) { var tests = []struct { stmt string expr *influxql.VarRef sub string err string }{ // 0. Single series { stmt: `SELECT value FROM myseries WHERE value > 1`, expr: &influxql.VarRef{Val: "value"}, sub: `SELECT value FROM myseries WHERE value > 1.000`, }, // 1. Simple join { stmt: `SELECT sum(aa.value) + sum(bb.value) FROM aa, bb`, expr: &influxql.VarRef{Val: "aa.value"}, sub: `SELECT "aa.value" FROM aa`, }, // 2. Simple merge { stmt: `SELECT sum(aa.value) + sum(bb.value) FROM aa, bb`, expr: &influxql.VarRef{Val: "bb.value"}, sub: `SELECT "bb.value" FROM bb`, }, // 3. Join with condition { stmt: `SELECT sum(aa.value) + sum(bb.value) FROM aa, bb WHERE aa.host = 'servera' AND bb.host = 'serverb'`, expr: &influxql.VarRef{Val: "bb.value"}, sub: `SELECT "bb.value" FROM bb WHERE "bb.host" = 'serverb'`, }, // 4. Join with complex condition { stmt: `SELECT sum(aa.value) + sum(bb.value) FROM aa, bb WHERE aa.host = 'servera' AND (bb.host = 'serverb' OR bb.host = 'serverc') AND 1 = 2`, expr: &influxql.VarRef{Val: "bb.value"}, sub: `SELECT "bb.value" FROM bb WHERE ("bb.host" = 'serverb' OR "bb.host" = 'serverc') AND 1.000 = 2.000`, }, // 5. 4 with different condition order { stmt: `SELECT sum(aa.value) + sum(bb.value) FROM aa, bb WHERE ((bb.host = 'serverb' OR bb.host = 'serverc') AND aa.host = 'servera') AND 1 = 2`, expr: &influxql.VarRef{Val: "bb.value"}, sub: `SELECT "bb.value" FROM bb WHERE (("bb.host" = 'serverb' OR "bb.host" = 'serverc')) AND 1.000 = 2.000`, }, } for i, tt := range tests { // Parse statement. stmt, err := influxql.NewParser(strings.NewReader(tt.stmt)).ParseStatement() if err != nil { t.Fatalf("invalid statement: %q: %s", tt.stmt, err) } // Extract substatement. sub, err := stmt.(*influxql.SelectStatement).Substatement(tt.expr) if err != nil { t.Errorf("%d. %q: unexpected error: %s", i, tt.stmt, err) continue } if substr := sub.String(); tt.sub != substr { t.Errorf("%d. %q: unexpected substatement:\n\nexp=%s\n\ngot=%s\n\n", i, tt.stmt, tt.sub, substr) continue } } } // Ensure the SELECT statement can extract GROUP BY interval. func TestSelectStatement_GroupByInterval(t *testing.T) { q := "SELECT sum(value) from foo where time < now() GROUP BY time(10m)" stmt, err := influxql.NewParser(strings.NewReader(q)).ParseStatement() if err != nil { t.Fatalf("invalid statement: %q: %s", stmt, err) } s := stmt.(*influxql.SelectStatement) d, err := s.GroupByInterval() if d != 10*time.Minute { t.Fatalf("group by interval not equal:\nexp=%s\ngot=%s", 10*time.Minute, d) } if err != nil { t.Fatalf("error parsing group by interval: %s", err.Error()) } } // Ensure the SELECT statement can have its start and end time set func TestSelectStatement_SetTimeRange(t *testing.T) { q := "SELECT sum(value) from foo where time < now() GROUP BY time(10m)" stmt, err := influxql.NewParser(strings.NewReader(q)).ParseStatement() if err != nil { t.Fatalf("invalid statement: %q: %s", stmt, err) } s := stmt.(*influxql.SelectStatement) min, max := influxql.TimeRange(s.Condition) start := time.Now().Add(-20 * time.Hour).Round(time.Second).UTC() end := time.Now().Add(10 * time.Hour).Round(time.Second).UTC() s.SetTimeRange(start, end) min, max = influxql.TimeRange(s.Condition) if min != start { t.Fatalf("start time wasn't set properly.\n exp: %s\n got: %s", start, min) } // the end range is actually one nanosecond before the given one since end is exclusive end = end.Add(-time.Nanosecond) if max != end { t.Fatalf("end time wasn't set properly.\n exp: %s\n got: %s", end, max) } // ensure we can set a time on a select that already has one set start = time.Now().Add(-20 * time.Hour).Round(time.Second).UTC() end = time.Now().Add(10 * time.Hour).Round(time.Second).UTC() q = fmt.Sprintf("SELECT sum(value) from foo WHERE time >= %ds and time <= %ds GROUP BY time(10m)", start.Unix(), end.Unix()) stmt, err = influxql.NewParser(strings.NewReader(q)).ParseStatement() if err != nil { t.Fatalf("invalid statement: %q: %s", stmt, err) } s = stmt.(*influxql.SelectStatement) min, max = influxql.TimeRange(s.Condition) if start != min || end != max { t.Fatalf("start and end times weren't equal:\n exp: %s\n got: %s\n exp: %s\n got:%s\n", start, min, end, max) } // update and ensure it saves it start = time.Now().Add(-40 * time.Hour).Round(time.Second).UTC() end = time.Now().Add(20 * time.Hour).Round(time.Second).UTC() s.SetTimeRange(start, end) min, max = influxql.TimeRange(s.Condition) // TODO: right now the SetTimeRange can't override the start time if it's more recent than what they're trying to set it to. // shouldn't matter for our purposes with continuous queries, but fix this later if min != start { t.Fatalf("start time wasn't set properly.\n exp: %s\n got: %s", start, min) } // the end range is actually one nanosecond before the given one since end is exclusive end = end.Add(-time.Nanosecond) if max != end { t.Fatalf("end time wasn't set properly.\n exp: %s\n got: %s", end, max) } // ensure that when we set a time range other where clause conditions are still there q = "SELECT sum(value) from foo WHERE foo = 'bar' and time < now() GROUP BY time(10m)" stmt, err = influxql.NewParser(strings.NewReader(q)).ParseStatement() if err != nil { t.Fatalf("invalid statement: %q: %s", stmt, err) } s = stmt.(*influxql.SelectStatement) // update and ensure it saves it start = time.Now().Add(-40 * time.Hour).Round(time.Second).UTC() end = time.Now().Add(20 * time.Hour).Round(time.Second).UTC() s.SetTimeRange(start, end) min, max = influxql.TimeRange(s.Condition) if min != start { t.Fatalf("start time wasn't set properly.\n exp: %s\n got: %s", start, min) } // the end range is actually one nanosecond before the given one since end is exclusive end = end.Add(-time.Nanosecond) if max != end { t.Fatalf("end time wasn't set properly.\n exp: %s\n got: %s", end, max) } // ensure the where clause is there hasWhere := false influxql.WalkFunc(s.Condition, func(n influxql.Node) { if ex, ok := n.(*influxql.BinaryExpr); ok { if lhs, ok := ex.LHS.(*influxql.VarRef); ok { if lhs.Val == "foo" { if rhs, ok := ex.RHS.(*influxql.StringLiteral); ok { if rhs.Val == "bar" { hasWhere = true } } } } } }) if !hasWhere { t.Fatal("set time range cleared out the where clause") } } // Ensure the idents from the select clause can come out func TestSelect_NamesInSelect(t *testing.T) { s := MustParseSelectStatement("select count(asdf), count(bar) from cpu") a := s.NamesInSelect() if !reflect.DeepEqual(a, []string{"asdf", "bar"}) { t.Fatal("expected names asdf and bar") } } // Ensure the idents from the where clause can come out func TestSelect_NamesInWhere(t *testing.T) { s := MustParseSelectStatement("select * from cpu where time > 23s AND (asdf = 'jkl' OR (foo = 'bar' AND baz = 'bar'))") a := s.NamesInWhere() if !reflect.DeepEqual(a, []string{"time", "asdf", "foo", "baz"}) { t.Fatalf("exp: time,asdf,foo,baz\ngot: %s\n", strings.Join(a, ",")) } } func TestSelectStatement_HasWildcard(t *testing.T) { var tests = []struct { stmt string wildcard bool }{ // No wildcards { stmt: `SELECT value FROM cpu`, wildcard: false, }, // Query wildcard { stmt: `SELECT * FROM cpu`, wildcard: true, }, // No GROUP BY wildcards { stmt: `SELECT value FROM cpu GROUP BY host`, wildcard: false, }, // No GROUP BY wildcards, time only { stmt: `SELECT mean(value) FROM cpu where time < now() GROUP BY time(5ms)`, wildcard: false, }, // GROUP BY wildcard { stmt: `SELECT value FROM cpu GROUP BY *`, wildcard: true, }, // GROUP BY wildcard with time { stmt: `SELECT mean(value) FROM cpu where time < now() GROUP BY *,time(1m)`, wildcard: true, }, // GROUP BY wildcard with explicit { stmt: `SELECT value FROM cpu GROUP BY *,host`, wildcard: true, }, // GROUP BY multiple wildcards { stmt: `SELECT value FROM cpu GROUP BY *,*`, wildcard: true, }, // Combo { stmt: `SELECT * FROM cpu GROUP BY *`, wildcard: true, }, } for i, tt := range tests { // Parse statement. stmt, err := influxql.NewParser(strings.NewReader(tt.stmt)).ParseStatement() if err != nil { t.Fatalf("invalid statement: %q: %s", tt.stmt, err) } // Test wildcard detection. if w := stmt.(*influxql.SelectStatement).HasWildcard(); tt.wildcard != w { t.Errorf("%d. %q: unexpected wildcard detection:\n\nexp=%v\n\ngot=%v\n\n", i, tt.stmt, tt.wildcard, w) continue } } } // Test SELECT statement wildcard rewrite. func TestSelectStatement_RewriteWildcards(t *testing.T) { var tests = []struct { stmt string rewrite string }{ // No wildcards { stmt: `SELECT value FROM cpu`, rewrite: `SELECT value FROM cpu`, }, // Query wildcard { stmt: `SELECT * FROM cpu`, rewrite: `SELECT host, region, value1, value2 FROM cpu`, }, // Parser fundamentally prohibits multiple query sources // Query wildcard with explicit // { // stmt: `SELECT *,value1 FROM cpu`, // rewrite: `SELECT value1, value2, value1 FROM cpu`, // }, // Query multiple wildcards // { // stmt: `SELECT *,* FROM cpu`, // rewrite: `SELECT value1,value2,value1,value2 FROM cpu`, // }, // No GROUP BY wildcards { stmt: `SELECT value FROM cpu GROUP BY host`, rewrite: `SELECT value FROM cpu GROUP BY host`, }, // No GROUP BY wildcards, time only { stmt: `SELECT mean(value) FROM cpu where time < now() GROUP BY time(5ms)`, rewrite: `SELECT mean(value) FROM cpu WHERE time < now() GROUP BY time(5ms)`, }, // GROUP BY wildcard { stmt: `SELECT value FROM cpu GROUP BY *`, rewrite: `SELECT value FROM cpu GROUP BY host, region`, }, // GROUP BY wildcard with time { stmt: `SELECT mean(value) FROM cpu where time < now() GROUP BY *,time(1m)`, rewrite: `SELECT mean(value) FROM cpu WHERE time < now() GROUP BY host, region, time(1m)`, }, // GROUP BY wildarde with fill { stmt: `SELECT mean(value) FROM cpu where time < now() GROUP BY *,time(1m) fill(0)`, rewrite: `SELECT mean(value) FROM cpu WHERE time < now() GROUP BY host, region, time(1m) fill(0)`, }, // GROUP BY wildcard with explicit { stmt: `SELECT value FROM cpu GROUP BY *,host`, rewrite: `SELECT value FROM cpu GROUP BY host, region, host`, }, // GROUP BY multiple wildcards { stmt: `SELECT value FROM cpu GROUP BY *,*`, rewrite: `SELECT value FROM cpu GROUP BY host, region, host, region`, }, // Combo { stmt: `SELECT * FROM cpu GROUP BY *`, rewrite: `SELECT value1, value2 FROM cpu GROUP BY host, region`, }, } for i, tt := range tests { // Parse statement. stmt, err := influxql.NewParser(strings.NewReader(tt.stmt)).ParseStatement() if err != nil { t.Fatalf("invalid statement: %q: %s", tt.stmt, err) } var ic IteratorCreator ic.FieldDimensionsFn = func(sources influxql.Sources) (fields, dimensions map[string]struct{}, err error) { fields = map[string]struct{}{"value1": struct{}{}, "value2": struct{}{}} dimensions = map[string]struct{}{"host": struct{}{}, "region": struct{}{}} return } // Rewrite statement. rw, err := stmt.(*influxql.SelectStatement).RewriteWildcards(&ic) if err != nil { t.Errorf("%d. %q: error: %s", i, tt.stmt, err) } else if rw == nil { t.Errorf("%d. %q: unexpected nil statement", i, tt.stmt) } else if rw := rw.String(); tt.rewrite != rw { t.Errorf("%d. %q: unexpected rewrite:\n\nexp=%s\n\ngot=%s\n\n", i, tt.stmt, tt.rewrite, rw) } } } // Ensure that the IsRawQuery flag gets set properly func TestSelectStatement_IsRawQuerySet(t *testing.T) { var tests = []struct { stmt string isRaw bool }{ { stmt: "select * from foo", isRaw: true, }, { stmt: "select value1,value2 from foo", isRaw: true, }, { stmt: "select value1,value2 from foo, time(10m)", isRaw: true, }, { stmt: "select mean(value) from foo where time < now() group by time(5m)", isRaw: false, }, { stmt: "select mean(value) from foo group by bar", isRaw: false, }, { stmt: "select mean(value) from foo group by *", isRaw: false, }, { stmt: "select mean(value) from foo group by *", isRaw: false, }, } for _, tt := range tests { s := MustParseSelectStatement(tt.stmt) if s.IsRawQuery != tt.isRaw { t.Errorf("'%s', IsRawQuery should be %v", tt.stmt, tt.isRaw) } } } func TestSelectStatement_HasDerivative(t *testing.T) { var tests = []struct { stmt string derivative bool }{ // No derivatives { stmt: `SELECT value FROM cpu`, derivative: false, }, // Query derivative { stmt: `SELECT derivative(value) FROM cpu`, derivative: true, }, // No GROUP BY time only { stmt: `SELECT mean(value) FROM cpu where time < now() GROUP BY time(5ms)`, derivative: false, }, // No GROUP BY derivatives, time only { stmt: `SELECT derivative(mean(value)) FROM cpu where time < now() GROUP BY time(5ms)`, derivative: true, }, { stmt: `SELECT value FROM cpu`, derivative: false, }, // Query derivative { stmt: `SELECT non_negative_derivative(value) FROM cpu`, derivative: true, }, // No GROUP BY derivatives, time only { stmt: `SELECT non_negative_derivative(mean(value)) FROM cpu where time < now() GROUP BY time(5ms)`, derivative: true, }, // Invalid derivative function name { stmt: `SELECT typoDerivative(value) FROM cpu where time < now()`, derivative: false, }, } for i, tt := range tests { // Parse statement. t.Logf("index: %d, statement: %s", i, tt.stmt) stmt, err := influxql.NewParser(strings.NewReader(tt.stmt)).ParseStatement() if err != nil { t.Fatalf("invalid statement: %q: %s", tt.stmt, err) } // Test derivative detection. if d := stmt.(*influxql.SelectStatement).HasDerivative(); tt.derivative != d { t.Errorf("%d. %q: unexpected derivative detection:\n\nexp=%v\n\ngot=%v\n\n", i, tt.stmt, tt.derivative, d) continue } } } func TestSelectStatement_IsSimpleDerivative(t *testing.T) { var tests = []struct { stmt string derivative bool }{ // No derivatives { stmt: `SELECT value FROM cpu`, derivative: false, }, // Query derivative { stmt: `SELECT derivative(value) FROM cpu`, derivative: true, }, // Query derivative { stmt: `SELECT non_negative_derivative(value) FROM cpu`, derivative: true, }, // No GROUP BY time only { stmt: `SELECT mean(value) FROM cpu where time < now() GROUP BY time(5ms)`, derivative: false, }, // No GROUP BY derivatives, time only { stmt: `SELECT non_negative_derivative(mean(value)) FROM cpu where time < now() GROUP BY time(5ms)`, derivative: false, }, // Invalid derivative function name { stmt: `SELECT typoDerivative(value) FROM cpu where time < now()`, derivative: false, }, } for i, tt := range tests { // Parse statement. t.Logf("index: %d, statement: %s", i, tt.stmt) stmt, err := influxql.NewParser(strings.NewReader(tt.stmt)).ParseStatement() if err != nil { t.Fatalf("invalid statement: %q: %s", tt.stmt, err) } // Test derivative detection. if d := stmt.(*influxql.SelectStatement).IsSimpleDerivative(); tt.derivative != d { t.Errorf("%d. %q: unexpected derivative detection:\n\nexp=%v\n\ngot=%v\n\n", i, tt.stmt, tt.derivative, d) continue } } } func TestSelectStatement_HasSimpleCount(t *testing.T) { var tests = []struct { stmt string count bool }{ // No counts { stmt: `SELECT value FROM cpu`, count: false, }, // Query count { stmt: `SELECT count(value) FROM cpu`, count: true, }, // No GROUP BY time only { stmt: `SELECT count(distinct(value)) FROM cpu where time < now() GROUP BY time(5ms)`, count: false, }, // Query count { stmt: `SELECT typoCount(value) FROM cpu`, count: false, }, // No GROUP BY time only { stmt: `SELECT typoCount(distinct(value)) FROM cpu where time < now() GROUP BY time(5ms)`, count: false, }, } for i, tt := range tests { // Parse statement. t.Logf("index: %d, statement: %s", i, tt.stmt) stmt, err := influxql.NewParser(strings.NewReader(tt.stmt)).ParseStatement() if err != nil { t.Fatalf("invalid statement: %q: %s", tt.stmt, err) } // Test count detection. if c := stmt.(*influxql.SelectStatement).HasSimpleCount(); tt.count != c { t.Errorf("%d. %q: unexpected count detection:\n\nexp=%v\n\ngot=%v\n\n", i, tt.stmt, tt.count, c) continue } } } func TestSelectStatement_HasCountDistinct(t *testing.T) { var tests = []struct { stmt string count bool }{ // No counts { stmt: `SELECT value FROM cpu`, count: false, }, // Query count { stmt: `SELECT count(value) FROM cpu`, count: false, }, // No GROUP BY time only { stmt: `SELECT count(distinct(value)) FROM cpu where time < now() GROUP BY time(5ms)`, count: true, }, // Query count { stmt: `SELECT typoCount(value) FROM cpu`, count: false, }, // No GROUP BY time only { stmt: `SELECT typoCount(distinct(value)) FROM cpu where time < now() GROUP BY time(5ms)`, count: false, }, } for i, tt := range tests { // Parse statement. t.Logf("index: %d, statement: %s", i, tt.stmt) stmt, err := influxql.NewParser(strings.NewReader(tt.stmt)).ParseStatement() if err != nil { t.Fatalf("invalid statement: %q: %s", tt.stmt, err) } // Test count detection. if c := stmt.(*influxql.SelectStatement).HasCountDistinct(); tt.count != c { t.Errorf("%d. %q: unexpected count detection:\n\nexp=%v\n\ngot=%v\n\n", i, tt.stmt, tt.count, c) continue } } } // Ensure the time range of an expression can be extracted. func TestTimeRange(t *testing.T) { for i, tt := range []struct { expr string min, max string }{ // LHS VarRef {expr: `time > '2000-01-01 00:00:00'`, min: `2000-01-01T00:00:00.000000001Z`, max: `0001-01-01T00:00:00Z`}, {expr: `time >= '2000-01-01 00:00:00'`, min: `2000-01-01T00:00:00Z`, max: `0001-01-01T00:00:00Z`}, {expr: `time < '2000-01-01 00:00:00'`, min: `0001-01-01T00:00:00Z`, max: `1999-12-31T23:59:59.999999999Z`}, {expr: `time <= '2000-01-01 00:00:00'`, min: `0001-01-01T00:00:00Z`, max: `2000-01-01T00:00:00Z`}, // RHS VarRef {expr: `'2000-01-01 00:00:00' > time`, min: `0001-01-01T00:00:00Z`, max: `1999-12-31T23:59:59.999999999Z`}, {expr: `'2000-01-01 00:00:00' >= time`, min: `0001-01-01T00:00:00Z`, max: `2000-01-01T00:00:00Z`}, {expr: `'2000-01-01 00:00:00' < time`, min: `2000-01-01T00:00:00.000000001Z`, max: `0001-01-01T00:00:00Z`}, {expr: `'2000-01-01 00:00:00' <= time`, min: `2000-01-01T00:00:00Z`, max: `0001-01-01T00:00:00Z`}, // number literal {expr: `time < 10`, min: `0001-01-01T00:00:00Z`, max: `1970-01-01T00:00:00.000000009Z`}, // Equality {expr: `time = '2000-01-01 00:00:00'`, min: `2000-01-01T00:00:00Z`, max: `2000-01-01T00:00:00.000000001Z`}, // Multiple time expressions. {expr: `time >= '2000-01-01 00:00:00' AND time < '2000-01-02 00:00:00'`, min: `2000-01-01T00:00:00Z`, max: `2000-01-01T23:59:59.999999999Z`}, // Min/max crossover {expr: `time >= '2000-01-01 00:00:00' AND time <= '1999-01-01 00:00:00'`, min: `2000-01-01T00:00:00Z`, max: `1999-01-01T00:00:00Z`}, // Absolute time {expr: `time = 1388534400s`, min: `2014-01-01T00:00:00Z`, max: `2014-01-01T00:00:00.000000001Z`}, // Non-comparative expressions. {expr: `time`, min: `0001-01-01T00:00:00Z`, max: `0001-01-01T00:00:00Z`}, {expr: `time + 2`, min: `0001-01-01T00:00:00Z`, max: `0001-01-01T00:00:00Z`}, {expr: `time - '2000-01-01 00:00:00'`, min: `0001-01-01T00:00:00Z`, max: `0001-01-01T00:00:00Z`}, {expr: `time AND '2000-01-01 00:00:00'`, min: `0001-01-01T00:00:00Z`, max: `0001-01-01T00:00:00Z`}, } { // Extract time range. expr := MustParseExpr(tt.expr) min, max := influxql.TimeRange(expr) // Compare with expected min/max. if min := min.Format(time.RFC3339Nano); tt.min != min { t.Errorf("%d. %s: unexpected min:\n\nexp=%s\n\ngot=%s\n\n", i, tt.expr, tt.min, min) continue } if max := max.Format(time.RFC3339Nano); tt.max != max { t.Errorf("%d. %s: unexpected max:\n\nexp=%s\n\ngot=%s\n\n", i, tt.expr, tt.max, max) continue } } } // Ensure that we see if a where clause has only time limitations func TestOnlyTimeExpr(t *testing.T) { var tests = []struct { stmt string exp bool }{ { stmt: `SELECT value FROM myseries WHERE value > 1`, exp: false, }, { stmt: `SELECT value FROM foo WHERE time >= '2000-01-01T00:00:05Z'`, exp: true, }, { stmt: `SELECT value FROM foo WHERE time >= '2000-01-01T00:00:05Z' AND time < '2000-01-01T00:00:05Z'`, exp: true, }, { stmt: `SELECT value FROM foo WHERE time >= '2000-01-01T00:00:05Z' AND asdf = 'bar'`, exp: false, }, { stmt: `SELECT value FROM foo WHERE asdf = 'jkl' AND (time >= '2000-01-01T00:00:05Z' AND time < '2000-01-01T00:00:05Z')`, exp: false, }, } for i, tt := range tests { // Parse statement. stmt, err := influxql.NewParser(strings.NewReader(tt.stmt)).ParseStatement() if err != nil { t.Fatalf("invalid statement: %q: %s", tt.stmt, err) } if influxql.OnlyTimeExpr(stmt.(*influxql.SelectStatement).Condition) != tt.exp { t.Fatalf("%d. expected statement to return only time dimension to be %t: %s", i, tt.exp, tt.stmt) } } } // Ensure an AST node can be rewritten. func TestRewrite(t *testing.T) { expr := MustParseExpr(`time > 1 OR foo = 2`) // Flip LHS & RHS in all binary expressions. act := influxql.RewriteFunc(expr, func(n influxql.Node) influxql.Node { switch n := n.(type) { case *influxql.BinaryExpr: return &influxql.BinaryExpr{Op: n.Op, LHS: n.RHS, RHS: n.LHS} default: return n } }) // Verify that everything is flipped. if act := act.String(); act != `2.000 = foo OR 1.000 > time` { t.Fatalf("unexpected result: %s", act) } } // Ensure that the String() value of a statement is parseable func TestParseString(t *testing.T) { var tests = []struct { stmt string }{ { stmt: `SELECT "cpu load" FROM myseries`, }, { stmt: `SELECT "cpu load" FROM "my series"`, }, { stmt: `SELECT "cpu\"load" FROM myseries`, }, { stmt: `SELECT "cpu'load" FROM myseries`, }, { stmt: `SELECT "cpu load" FROM "my\"series"`, }, { stmt: `SELECT "field with spaces" FROM "\"ugly\" db"."\"ugly\" rp"."\"ugly\" measurement"`, }, { stmt: `SELECT * FROM myseries`, }, { stmt: `DROP DATABASE "!"`, }, { stmt: `DROP RETENTION POLICY "my rp" ON "a database"`, }, { stmt: `CREATE RETENTION POLICY "my rp" ON "a database" DURATION 1d REPLICATION 1`, }, { stmt: `ALTER RETENTION POLICY "my rp" ON "a database" DEFAULT`, }, { stmt: `SHOW RETENTION POLICIES ON "a database"`, }, { stmt: `SHOW TAG VALUES WITH KEY IN ("a long name", short)`, }, { stmt: `DROP CONTINUOUS QUERY "my query" ON "my database"`, }, // See issues https://github.com/influxdata/influxdb/issues/1647 // and https://github.com/influxdata/influxdb/issues/4404 //{ // stmt: `DELETE FROM "my db"."my rp"."my measurement"`, //}, { stmt: `DROP SUBSCRIPTION "ugly \"subscription\" name" ON "\"my\" db"."\"my\" rp"`, }, { stmt: `CREATE SUBSCRIPTION "ugly \"subscription\" name" ON "\"my\" db"."\"my\" rp" DESTINATIONS ALL 'my host', 'my other host'`, }, { stmt: `SHOW MEASUREMENTS WITH MEASUREMENT =~ /foo/`, }, { stmt: `SHOW MEASUREMENTS WITH MEASUREMENT = "and/or"`, }, { stmt: `DROP USER "user with spaces"`, }, { stmt: `GRANT ALL PRIVILEGES ON "db with spaces" TO "user with spaces"`, }, { stmt: `GRANT ALL PRIVILEGES TO "user with spaces"`, }, { stmt: `SHOW GRANTS FOR "user with spaces"`, }, { stmt: `REVOKE ALL PRIVILEGES ON "db with spaces" FROM "user with spaces"`, }, { stmt: `REVOKE ALL PRIVILEGES FROM "user with spaces"`, }, { stmt: `CREATE DATABASE "db with spaces"`, }, } for _, tt := range tests { // Parse statement. stmt, err := influxql.NewParser(strings.NewReader(tt.stmt)).ParseStatement() if err != nil { t.Fatalf("invalid statement: %q: %s", tt.stmt, err) } stmtCopy, err := influxql.NewParser(strings.NewReader(stmt.String())).ParseStatement() if err != nil { t.Fatalf("failed to parse string: %v\norig: %v\ngot: %v", err, tt.stmt, stmt.String()) } if !reflect.DeepEqual(stmt, stmtCopy) { t.Fatalf("statement changed after stringifying and re-parsing:\noriginal : %v\nre-parsed: %v\n", tt.stmt, stmtCopy.String()) } } } // Ensure an expression can be reduced. func TestEval(t *testing.T) { for i, tt := range []struct { in string out interface{} data map[string]interface{} }{ // Number literals. {in: `1 + 2`, out: float64(3)}, {in: `(foo*2) + ( (4/2) + (3 * 5) - 0.5 )`, out: float64(26.5), data: map[string]interface{}{"foo": float64(5)}}, {in: `foo / 2`, out: float64(2), data: map[string]interface{}{"foo": float64(4)}}, {in: `4 = 4`, out: true}, {in: `4 <> 4`, out: false}, {in: `6 > 4`, out: true}, {in: `4 >= 4`, out: true}, {in: `4 < 6`, out: true}, {in: `4 <= 4`, out: true}, {in: `4 AND 5`, out: nil}, // Boolean literals. {in: `true AND false`, out: false}, {in: `true OR false`, out: true}, // String literals. {in: `'foo' = 'bar'`, out: false}, {in: `'foo' = 'foo'`, out: true}, // Variable references. {in: `foo`, out: "bar", data: map[string]interface{}{"foo": "bar"}}, {in: `foo = 'bar'`, out: true, data: map[string]interface{}{"foo": "bar"}}, {in: `foo = 'bar'`, out: nil, data: map[string]interface{}{"foo": nil}}, {in: `foo <> 'bar'`, out: true, data: map[string]interface{}{"foo": "xxx"}}, } { // Evaluate expression. out := influxql.Eval(MustParseExpr(tt.in), tt.data) // Compare with expected output. if !reflect.DeepEqual(tt.out, out) { t.Errorf("%d. %s: unexpected output:\n\nexp=%#v\n\ngot=%#v\n\n", i, tt.in, tt.out, out) continue } } } // Ensure an expression can be reduced. func TestReduce(t *testing.T) { now := mustParseTime("2000-01-01T00:00:00Z") for i, tt := range []struct { in string out string data Valuer }{ // Number literals. {in: `1 + 2`, out: `3.000`}, {in: `(foo*2) + ( (4/2) + (3 * 5) - 0.5 )`, out: `(foo * 2.000) + 16.500`}, {in: `foo(bar(2 + 3), 4)`, out: `foo(bar(5.000), 4.000)`}, {in: `4 / 0`, out: `0.000`}, {in: `4 = 4`, out: `true`}, {in: `4 <> 4`, out: `false`}, {in: `6 > 4`, out: `true`}, {in: `4 >= 4`, out: `true`}, {in: `4 < 6`, out: `true`}, {in: `4 <= 4`, out: `true`}, {in: `4 AND 5`, out: `4.000 AND 5.000`}, // Boolean literals. {in: `true AND false`, out: `false`}, {in: `true OR false`, out: `true`}, {in: `true OR (foo = bar AND 1 > 2)`, out: `true`}, {in: `(foo = bar AND 1 > 2) OR true`, out: `true`}, {in: `false OR (foo = bar AND 1 > 2)`, out: `false`}, {in: `(foo = bar AND 1 > 2) OR false`, out: `false`}, {in: `true = false`, out: `false`}, {in: `true <> false`, out: `true`}, {in: `true + false`, out: `true + false`}, // Time literals. {in: `now() + 2h`, out: `'2000-01-01T02:00:00Z'`, data: map[string]interface{}{"now()": now}}, {in: `now() / 2h`, out: `'2000-01-01T00:00:00Z' / 2h`, data: map[string]interface{}{"now()": now}}, {in: `4ยต + now()`, out: `'2000-01-01T00:00:00.000004Z'`, data: map[string]interface{}{"now()": now}}, {in: `now() = now()`, out: `true`, data: map[string]interface{}{"now()": now}}, {in: `now() <> now()`, out: `false`, data: map[string]interface{}{"now()": now}}, {in: `now() < now() + 1h`, out: `true`, data: map[string]interface{}{"now()": now}}, {in: `now() <= now() + 1h`, out: `true`, data: map[string]interface{}{"now()": now}}, {in: `now() >= now() - 1h`, out: `true`, data: map[string]interface{}{"now()": now}}, {in: `now() > now() - 1h`, out: `true`, data: map[string]interface{}{"now()": now}}, {in: `now() - (now() - 60s)`, out: `1m`, data: map[string]interface{}{"now()": now}}, {in: `now() AND now()`, out: `'2000-01-01T00:00:00Z' AND '2000-01-01T00:00:00Z'`, data: map[string]interface{}{"now()": now}}, {in: `now()`, out: `now()`}, // Duration literals. {in: `10m + 1h - 60s`, out: `69m`}, {in: `(10m / 2) * 5`, out: `25m`}, {in: `60s = 1m`, out: `true`}, {in: `60s <> 1m`, out: `false`}, {in: `60s < 1h`, out: `true`}, {in: `60s <= 1h`, out: `true`}, {in: `60s > 12s`, out: `true`}, {in: `60s >= 1m`, out: `true`}, {in: `60s AND 1m`, out: `1m AND 1m`}, {in: `60m / 0`, out: `0s`}, {in: `60m + 50`, out: `1h + 50.000`}, // String literals. {in: `'foo' + 'bar'`, out: `'foobar'`}, // Variable references. {in: `foo`, out: `'bar'`, data: map[string]interface{}{"foo": "bar"}}, {in: `foo = 'bar'`, out: `true`, data: map[string]interface{}{"foo": "bar"}}, {in: `foo = 'bar'`, out: `false`, data: map[string]interface{}{"foo": nil}}, {in: `foo <> 'bar'`, out: `false`, data: map[string]interface{}{"foo": nil}}, } { // Fold expression. expr := influxql.Reduce(MustParseExpr(tt.in), tt.data) // Compare with expected output. if out := expr.String(); tt.out != out { t.Errorf("%d. %s: unexpected expr:\n\nexp=%s\n\ngot=%s\n\n", i, tt.in, tt.out, out) continue } } } func Test_fieldsNames(t *testing.T) { for _, test := range []struct { in []string out []string alias []string }{ { //case: binary expr(valRef) in: []string{"value+value"}, out: []string{"value", "value"}, alias: []string{""}, }, { //case: binary expr + valRef in: []string{"value+value", "temperature"}, out: []string{"value", "value", "temperature"}, alias: []string{"", "temperature"}, }, { //case: aggregate expr in: []string{"mean(value)"}, out: []string{"mean"}, alias: []string{"mean"}, }, { //case: binary expr(aggregate expr) in: []string{"mean(value) + max(value)"}, out: []string{"value", "value"}, alias: []string{""}, }, { //case: binary expr(aggregate expr) + valRef in: []string{"mean(value) + max(value)", "temperature"}, out: []string{"value", "value", "temperature"}, alias: []string{"", "temperature"}, }, { //case: mixed aggregate and varRef in: []string{"mean(value) + temperature"}, out: []string{"value", "temperature"}, alias: []string{""}, }, { //case: ParenExpr(varRef) in: []string{"(value)"}, out: []string{"value"}, alias: []string{""}, }, { //case: ParenExpr(varRef + varRef) in: []string{"(value + value)"}, out: []string{"value", "value"}, alias: []string{""}, }, { //case: ParenExpr(aggregate) in: []string{"(mean(value))"}, out: []string{"value"}, alias: []string{""}, }, { //case: ParenExpr(aggregate + aggregate) in: []string{"(mean(value) + max(value))"}, out: []string{"value", "value"}, alias: []string{""}, }, } { fields := influxql.Fields{} for _, s := range test.in { expr := MustParseExpr(s) fields = append(fields, &influxql.Field{Expr: expr}) } got := fields.Names() if !reflect.DeepEqual(got, test.out) { t.Errorf("get fileds name:\nexp=%v\ngot=%v\n", test.out, got) } alias := fields.AliasNames() if !reflect.DeepEqual(alias, test.alias) { t.Errorf("get fileds alias name:\nexp=%v\ngot=%v\n", test.alias, alias) } } } func TestSources_Names(t *testing.T) { sources := influxql.Sources([]influxql.Source{ &influxql.Measurement{ Name: "cpu", }, &influxql.Measurement{ Name: "mem", }, }) names := sources.Names() if names[0] != "cpu" { t.Errorf("expected cpu, got %s", names[0]) } if names[1] != "mem" { t.Errorf("expected mem, got %s", names[1]) } } func TestSources_HasSystemSource(t *testing.T) { sources := influxql.Sources([]influxql.Source{ &influxql.Measurement{ Name: "_measurements", }, }) ok := sources.HasSystemSource() if !ok { t.Errorf("expected to find a system source, found none") } sources = influxql.Sources([]influxql.Source{ &influxql.Measurement{ Name: "cpu", }, }) ok = sources.HasSystemSource() if ok { t.Errorf("expected to find no system source, found one") } } // Valuer represents a simple wrapper around a map to implement the influxql.Valuer interface. type Valuer map[string]interface{} // Value returns the value and existence of a key. func (o Valuer) Value(key string) (v interface{}, ok bool) { v, ok = o[key] return } // mustParseTime parses an IS0-8601 string. Panic on error. func mustParseTime(s string) time.Time { t, err := time.Parse(time.RFC3339, s) if err != nil { panic(err.Error()) } return t }