diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index 34bbfdc6865..5545168afbb 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -74,6 +74,19 @@ func (m *MySqlMacroEngine) EvaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("missing time column argument for macro %v", name) } return fmt.Sprintf("%s >= FROM_UNIXTIME(%d) AND %s <= FROM_UNIXTIME(%d)", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil + case "__timeFrom": + return fmt.Sprintf("FROM_UNIXTIME(%d)", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil + case "__timeTo": + return fmt.Sprintf("FROM_UNIXTIME(%d)", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil + case "__unixEpochFilter": + if len(args) == 0 { + return "", fmt.Errorf("missing time column argument for macro %v", name) + } + return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil + case "__unixEpochFrom": + return fmt.Sprintf("%d", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil + case "__unixEpochTo": + return fmt.Sprintf("%d", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil default: return "", fmt.Errorf("Unknown macro %v", name) } diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go index 9684daac4e8..d85133afa84 100644 --- a/pkg/tsdb/mysql/macros_test.go +++ b/pkg/tsdb/mysql/macros_test.go @@ -39,5 +39,60 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, "WHERE time_column >= FROM_UNIXTIME(18446744066914186738) AND time_column <= FROM_UNIXTIME(18446744066914187038)") }) + Convey("interpolate __timeFrom function", func() { + engine := &MySqlMacroEngine{ + TimeRange: &tsdb.TimeRange{From: "5m", To: "now"}, + } + + sql, err := engine.Interpolate("select $__timeFrom(time_column)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select FROM_UNIXTIME(18446744066914186738)") + }) + + Convey("interpolate __timeTo function", func() { + engine := &MySqlMacroEngine{ + TimeRange: &tsdb.TimeRange{From: "5m", To: "now"}, + } + + sql, err := engine.Interpolate("select $__timeTo(time_column)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select FROM_UNIXTIME(18446744066914187038)") + }) + + Convey("interpolate __unixEpochFilter function", func() { + engine := &MySqlMacroEngine{ + TimeRange: &tsdb.TimeRange{From: "5m", To: "now"}, + } + + sql, err := engine.Interpolate("select $__unixEpochFilter(18446744066914186738)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select 18446744066914186738 >= 18446744066914186738 AND 18446744066914186738 <= 18446744066914187038") + }) + + Convey("interpolate __unixEpochFrom function", func() { + engine := &MySqlMacroEngine{ + TimeRange: &tsdb.TimeRange{From: "5m", To: "now"}, + } + + sql, err := engine.Interpolate("select $__unixEpochFrom()") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select 18446744066914186738") + }) + + Convey("interpolate __unixEpochTo function", func() { + engine := &MySqlMacroEngine{ + TimeRange: &tsdb.TimeRange{From: "5m", To: "now"}, + } + + sql, err := engine.Interpolate("select $__unixEpochTo()") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select 18446744066914187038") + }) + }) } diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 367124a46b8..e38f37b96af 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -205,6 +205,8 @@ func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) values[i] = new(float32) case mysql.FieldTypeNameNewDecimal: values[i] = new(float64) + case mysql.FieldTypeNameFloat: + values[i] = new(float64) case mysql.FieldTypeNameTimestamp: values[i] = new(time.Time) case mysql.FieldTypeNameDateTime: @@ -215,6 +217,20 @@ func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) values[i] = new(int16) case mysql.FieldTypeNameNULL: values[i] = nil + case mysql.FieldTypeNameBit: + values[i] = new([]byte) + case mysql.FieldTypeNameBLOB: + values[i] = new(string) + case mysql.FieldTypeNameTinyBLOB: + values[i] = new(string) + case mysql.FieldTypeNameMediumBLOB: + values[i] = new(string) + case mysql.FieldTypeNameLongBLOB: + values[i] = new(string) + case mysql.FieldTypeNameString: + values[i] = new(string) + case mysql.FieldTypeNameDate: + values[i] = new(string) default: return nil, fmt.Errorf("Database type %s not supported", stype.DatabaseTypeName()) } diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go new file mode 100644 index 00000000000..988833a7a1f --- /dev/null +++ b/pkg/tsdb/mysql/mysql_test.go @@ -0,0 +1,124 @@ +package mysql + +import ( + "testing" + "time" + + "github.com/go-xorm/xorm" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" + "github.com/grafana/grafana/pkg/tsdb" + . "github.com/smartystreets/goconvey/convey" +) + +// To run this test, remove the Skip from SkipConvey +// and set up a MySQL db named grafana_tests and a user/password grafana/password +func TestMySQL(t *testing.T) { + SkipConvey("MySQL", t, func() { + x := InitMySQLTestDB(t) + + executor := &MysqlExecutor{ + engine: x, + log: log.New("tsdb.mysql"), + } + + sess := x.NewSession() + defer sess.Close() + db := sess.DB() + + sql := "CREATE TABLE `mysql_types` (" + sql += "`atinyint` tinyint(1)," + sql += "`avarchar` varchar(3)," + sql += "`achar` char(3)," + sql += "`amediumint` mediumint," + sql += "`asmallint` smallint," + sql += "`abigint` bigint," + sql += "`aint` int(11)," + sql += "`adouble` double(10,2)," + sql += "`anewdecimal` decimal(10,2)," + sql += "`afloat` float(10,2)," + sql += "`atimestamp` timestamp NOT NULL," + sql += "`adatetime` datetime," + sql += "`atime` time," + // sql += "`ayear` year," // Crashes xorm when running cleandb + sql += "`abit` bit(1)," + sql += "`atinytext` tinytext," + sql += "`atinyblob` tinyblob," + sql += "`atext` text," + sql += "`ablob` blob," + sql += "`amediumtext` mediumtext," + sql += "`amediumblob` mediumblob," + sql += "`alongtext` longtext," + sql += "`alongblob` longblob," + sql += "`aenum` enum('val1', 'val2')," + sql += "`aset` set('a', 'b', 'c', 'd')," + sql += "`adate` date" + sql += ") ENGINE=InnoDB DEFAULT CHARSET=latin1;" + _, err := sess.Exec(sql) + So(err, ShouldBeNil) + + sql = "INSERT INTO `mysql_types` " + sql += "(`atinyint`, `avarchar`, `achar`, `amediumint`, `asmallint`, `abigint`, `aint`, `adouble`, " + sql += "`anewdecimal`, `afloat`, `adatetime`, `atimestamp`, `atime`, `abit`, `atinytext`, " + sql += "`atinyblob`, `atext`, `ablob`, `amediumtext`, `amediumblob`, `alongtext`, `alongblob`, " + sql += "`aenum`, `aset`, `adate`) " + sql += "VALUES(1, 'abc', 'def', 1, 10, 100, 1420070400, 1.11, " + sql += "2.22, 3.33, now(), current_timestamp(), '11:11:11', 1, 'tinytext', " + sql += "'tinyblob', 'text', 'blob', 'mediumtext', 'mediumblob', 'longtext', 'longblob', " + sql += "'val2', 'a,b', curdate());" + _, err = sess.Exec(sql) + So(err, ShouldBeNil) + + Convey("TransformToTable should map MySQL column types to Go types", func() { + rows, err := db.Query("SELECT * FROM mysql_types") + defer rows.Close() + So(err, ShouldBeNil) + + queryResult := &tsdb.QueryResult{Meta: simplejson.New()} + err = executor.TransformToTable(nil, rows, queryResult) + So(err, ShouldBeNil) + column := queryResult.Tables[0].Rows[0] + So(*column[0].(*int8), ShouldEqual, 1) + So(*column[1].(*string), ShouldEqual, "abc") + So(*column[2].(*string), ShouldEqual, "def") + So(*column[3].(*int32), ShouldEqual, 1) + So(*column[4].(*int16), ShouldEqual, 10) + So(*column[5].(*int64), ShouldEqual, 100) + So(*column[6].(*int), ShouldEqual, 1420070400) + So(*column[7].(*float64), ShouldEqual, 1.11) + So(*column[8].(*float64), ShouldEqual, 2.22) + So(*column[9].(*float64), ShouldEqual, 3.33) + _, offset := time.Now().Zone() + So((*column[10].(*time.Time)), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second)) + So(*column[11].(*time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second)) + So(*column[12].(*string), ShouldEqual, "11:11:11") + So(*column[13].(*[]byte), ShouldHaveSameTypeAs, []byte{1}) + So(*column[14].(*string), ShouldEqual, "tinytext") + So(*column[15].(*string), ShouldEqual, "tinyblob") + So(*column[16].(*string), ShouldEqual, "text") + So(*column[17].(*string), ShouldEqual, "blob") + So(*column[18].(*string), ShouldEqual, "mediumtext") + So(*column[19].(*string), ShouldEqual, "mediumblob") + So(*column[20].(*string), ShouldEqual, "longtext") + So(*column[21].(*string), ShouldEqual, "longblob") + So(*column[22].(*string), ShouldEqual, "val2") + So(*column[23].(*string), ShouldEqual, "a,b") + So(*column[24].(*string), ShouldEqual, time.Now().Format("2006-01-02T00:00:00Z")) + }) + }) +} + +func InitMySQLTestDB(t *testing.T) *xorm.Engine { + x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr+"&parseTime=true") + + // x.ShowSQL() + + if err != nil { + t.Fatalf("Failed to init mysql db %v", err) + } + + sqlutil.CleanDB(x) + + return x +} diff --git a/public/app/plugins/datasource/mysql/partials/annotations.editor.html b/public/app/plugins/datasource/mysql/partials/annotations.editor.html index 2f0bfd936a5..09581e2a552 100644 --- a/public/app/plugins/datasource/mysql/partials/annotations.editor.html +++ b/public/app/plugins/datasource/mysql/partials/annotations.editor.html @@ -28,7 +28,14 @@ An annotation is an event that is overlayed on top of graphs. The query can have Macros: - $__time(column) -> UNIX_TIMESTAMP(column) as time_sec -- $__timeFilter(column) -> UNIX_TIMESTAMP(time_date_time) > from AND UNIX_TIMESTAMP(time_date_time) < 1492750877 +- $__timeFilter(column) -> UNIX_TIMESTAMP(time_date_time) > 1492750877 AND UNIX_TIMESTAMP(time_date_time) < 1492750877 +- $__unixEpochFilter(column) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877 + +Or build your own conditionals using these macros which just return the values: +- $__timeFrom() -> FROM_UNIXTIME(1492750877) +- $__timeTo() -> FROM_UNIXTIME(1492750877) +- $__unixEpochFrom() -> 1492750877 +- $__unixEpochTo() -> 1492750877 diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index c7b90ad7815..32c3b097be3 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -46,7 +46,14 @@ Table: Macros: - $__time(column) -> UNIX_TIMESTAMP(column) as time_sec -- $__timeFilter(column) -> UNIX_TIMESTAMP(time_date_time) ≥ from AND UNIX_TIMESTAMP(time_date_time) ≤ 1492750877 +- $__timeFilter(column) -> UNIX_TIMESTAMP(time_date_time) ≥ 1492750877 AND UNIX_TIMESTAMP(time_date_time) ≤ 1492750877 +- $__unixEpochFilter(column) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877 + +Or build your own conditionals using these macros which just return the values: +- $__timeFrom() -> FROM_UNIXTIME(1492750877) +- $__timeTo() -> FROM_UNIXTIME(1492750877) +- $__unixEpochFrom() -> 1492750877 +- $__unixEpochTo() -> 1492750877