diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index 3e82dc6ca43..5ae27867c44 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -51,14 +51,25 @@ type QueryResult struct { RefId string `json:"refId"` Meta *simplejson.Json `json:"meta,omitempty"` Series TimeSeriesSlice `json:"series"` + Tables []*Table `json:"tables"` } type TimeSeries struct { Name string `json:"name"` Points TimeSeriesPoints `json:"points"` - Tags map[string]string `json:"tags"` + Tags map[string]string `json:"tags,omitempty"` } +type Table struct { + Columns []TableColumn `json:"columns"` + Rows []RowValues `json:"rows"` +} + +type TableColumn struct { + Text string `json:"text"` +} + +type RowValues []interface{} type TimePoint [2]null.Float type TimeSeriesPoints []TimePoint type TimeSeriesSlice []*TimeSeries diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index d655b816520..35203339af8 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -7,6 +7,7 @@ import ( "strconv" "sync" + "github.com/go-sql-driver/mysql" "github.com/go-xorm/core" "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/components/null" @@ -114,34 +115,97 @@ func (e *MysqlExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, co format := query.Model.Get("format").MustString("time_series") - if format == "time_series" { - res, err := e.TransformToTimeSeries(query, rows) + switch format { + case "time_series": + err := e.TransformToTimeSeries(query, rows, queryResult) if err != nil { queryResult.Error = err - return result + continue + } + case "table": + err := e.TransformToTable(query, rows, queryResult) + if err != nil { + queryResult.Error = err + continue } - - queryResult.Series = res - queryResult.Meta.Set("rowCount", countPointsInAllSeries(res)) } } return result } -func countPointsInAllSeries(seriesList tsdb.TimeSeriesSlice) (count int) { - for _, series := range seriesList { - count += len(series.Points) +func (e MysqlExecutor) TransformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error { + columnNames, err := rows.Columns() + columnCount := len(columnNames) + + if err != nil { + return err } - return count + + table := &tsdb.Table{ + Columns: make([]tsdb.TableColumn, columnCount), + Rows: make([]tsdb.RowValues, 0), + } + + for i, name := range columnNames { + table.Columns[i].Text = name + } + + columnTypes, err := rows.ColumnTypes() + if err != nil { + return err + } + + rowLimit := 1000000 + rowCount := 0 + + for ; rows.Next(); rowCount += 1 { + if rowCount > rowLimit { + return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit) + } + + values, err := e.getTypedRowData(columnTypes, rows) + if err != nil { + return err + } + + table.Rows = append(table.Rows, values) + } + + result.Tables = append(result.Tables, table) + result.Meta.Set("rowCount", rowCount) + return nil } -func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows) (tsdb.TimeSeriesSlice, error) { +func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) { + values := make([]interface{}, len(types)) + + for i, stype := range types { + switch stype.DatabaseTypeName() { + case mysql.FieldTypeNameVarString: + values[i] = new(string) + case mysql.FieldTypeNameLongLong: + values[i] = new(int64) + case mysql.FieldTypeNameDouble: + values[i] = new(float64) + default: + return nil, fmt.Errorf("Database type %s not supported", stype) + } + } + + if err := rows.Scan(values...); err != nil { + return nil, err + } + + return values, nil +} + +func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error { pointsBySeries := make(map[string]*tsdb.TimeSeries) columnNames, err := rows.Columns() if err != nil { - return nil, err + return err } rowData := NewStringStringScan(columnNames) @@ -150,13 +214,13 @@ func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows) for ; rows.Next(); rowCount += 1 { if rowCount > rowLimit { - return nil, fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit) + return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit) } err := rowData.Update(rows.Rows) if err != nil { e.log.Error("MySQL response parsing", "error", err) - return nil, fmt.Errorf("MySQL response parsing error %v", err) + return fmt.Errorf("MySQL response parsing error %v", err) } if rowData.metric == "" { @@ -166,7 +230,7 @@ func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows) //e.log.Debug("Rows", "metric", rowData.metric, "time", rowData.time, "value", rowData.value) if !rowData.time.Valid { - return nil, fmt.Errorf("Found row with no time value") + return fmt.Errorf("Found row with no time value") } if series, exist := pointsBySeries[rowData.metric]; exist { @@ -178,13 +242,12 @@ func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows) } } - seriesList := make(tsdb.TimeSeriesSlice, 0) for _, value := range pointsBySeries { - seriesList = append(seriesList, value) + result.Series = append(result.Series, value) } - e.log.Debug("TransformToTimeSeries", "rowCount", rowCount, "timeSeriesCount", len(seriesList)) - return seriesList, nil + result.Meta.Set("rowCount", rowCount) + return nil } type stringStringScan struct { diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 00050870088..af9d06d8742 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -31,6 +31,7 @@ class MetricsPanelCtrl extends PanelCtrl { skipDataOnInit: boolean; dataStream: any; dataSubscription: any; + dataList: any; constructor($scope, $injector) { super($scope, $injector); diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index ea3273bf390..70799048319 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -38,14 +38,20 @@ export class MysqlDatasource { to: options.range.to.valueOf().toString(), queries: queries, } - }).then(res => { - var data = []; + }).then(this.processQueryResult.bind(this)); + } - if (!res.data.results) { - return {data: data}; - } + processQueryResult(res) { + var data = []; - _.forEach(res.data.results, queryRes => { + if (!res.data.results) { + return {data: data}; + } + + for (let key in res.data.results) { + let queryRes = res.data.results[key]; + + if (queryRes.series) { for (let series of queryRes.series) { data.push({ target: series.name, @@ -54,10 +60,19 @@ export class MysqlDatasource { meta: queryRes.meta, }); } - }); + } - return {data: data}; - }); + if (queryRes.tables) { + for (let table of queryRes.tables) { + table.type = 'table'; + table.refId = queryRes.refId; + table.meta = queryRes.meta; + data.push(table); + } + } + } + + return {data: data}; } } diff --git a/public/app/plugins/datasource/mysql/module.ts b/public/app/plugins/datasource/mysql/module.ts index 6ed971287fd..6725a4fe0a6 100644 --- a/public/app/plugins/datasource/mysql/module.ts +++ b/public/app/plugins/datasource/mysql/module.ts @@ -27,7 +27,7 @@ class MysqlQueryCtrl extends QueryCtrl { constructor($scope, $injector) { super($scope, $injector); - this.target.format = 'time_series'; + this.target.format = this.target.format || 'time_series'; this.target.alias = ""; this.formats = [ {text: 'Time series', value: 'time_series'}, diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index 8a4a4520352..3dba49f9a89 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -16,24 +16,17 @@ - -
-
-
- - -
-
+
-
+
-
+
{{ctrl.lastQueryMeta.sql}}
{{ctrl.lastQueryError}}
diff --git a/vendor/github.com/go-sql-driver/mysql/row_columntypes.go b/vendor/github.com/go-sql-driver/mysql/row_columntypes.go new file mode 100644 index 00000000000..1dbfe472e7b --- /dev/null +++ b/vendor/github.com/go-sql-driver/mysql/row_columntypes.go @@ -0,0 +1,78 @@ +package mysql + +const ( + // In case we get something unexpected + FieldTypeUnknown = "UNKNOWN" + + // Human-readable names for each distinct type byte + FieldTypeNameDecimal = "DECIMAL" + FieldTypeNameTiny = "TINY" + FieldTypeNameShort = "SHORT" + FieldTypeNameLong = "LONG" + FieldTypeNameFloat = "FLOAT" + FieldTypeNameDouble = "DOUBLE" + FieldTypeNameNULL = "NULL" + FieldTypeNameTimestamp = "TIMESTAMP" + FieldTypeNameLongLong = "LONGLONG" + FieldTypeNameInt24 = "INT24" + FieldTypeNameDate = "DATE" + FieldTypeNameTime = "TIME" + FieldTypeNameDateTime = "DATETIME" + FieldTypeNameYear = "YEAR" + FieldTypeNameNewDate = "NEWDATE" + FieldTypeNameVarChar = "VARCHAR" + FieldTypeNameBit = "BIT" + FieldTypeNameJSON = "JSON" + FieldTypeNameNewDecimal = "NEWDECIMAL" + FieldTypeNameEnum = "ENUM" + FieldTypeNameSet = "SET" + FieldTypeNameTinyBLOB = "TINYBLOB" + FieldTypeNameMediumBLOB = "MEDIUMBLOB" + FieldTypeNameLongBLOB = "LONGBLOB" + FieldTypeNameBLOB = "BLOB" + FieldTypeNameVarString = "VARSTRING" + FieldTypeNameString = "STRING" + FieldTypeNameGeometry = "GEOMETRY" +) + +// mapping from each type identifier to human readable string +var mysqlTypeMap = map[byte]string{ + fieldTypeDecimal: FieldTypeNameDecimal, + fieldTypeTiny: FieldTypeNameTiny, + fieldTypeShort: FieldTypeNameShort, + fieldTypeLong: FieldTypeNameLong, + fieldTypeFloat: FieldTypeNameFloat, + fieldTypeDouble: FieldTypeNameDouble, + fieldTypeNULL: FieldTypeNameNULL, + fieldTypeTimestamp: FieldTypeNameTimestamp, + fieldTypeLongLong: FieldTypeNameLongLong, + fieldTypeInt24: FieldTypeNameInt24, + fieldTypeDate: FieldTypeNameDate, + fieldTypeTime: FieldTypeNameTime, + fieldTypeDateTime: FieldTypeNameDateTime, + fieldTypeYear: FieldTypeNameYear, + fieldTypeNewDate: FieldTypeNameNewDate, + fieldTypeVarChar: FieldTypeNameVarChar, + fieldTypeBit: FieldTypeNameBit, + fieldTypeJSON: FieldTypeNameJSON, + fieldTypeNewDecimal: FieldTypeNameNewDecimal, + fieldTypeEnum: FieldTypeNameEnum, + fieldTypeSet: FieldTypeNameSet, + fieldTypeTinyBLOB: FieldTypeNameTinyBLOB, + fieldTypeMediumBLOB: FieldTypeNameMediumBLOB, + fieldTypeLongBLOB: FieldTypeNameLongBLOB, + fieldTypeBLOB: FieldTypeNameBLOB, + fieldTypeVarString: FieldTypeNameVarString, + fieldTypeString: FieldTypeNameString, + fieldTypeGeometry: FieldTypeNameGeometry, +} + +// Make Rows implement the optional RowsColumnTypeDatabaseTypeName interface. +// See https://github.com/golang/go/commit/2a85578b0ecd424e95b29d810b7a414a299fd6a7 +// - (go 1.8 required for this to have any effect) +func (rows *mysqlRows) ColumnTypeDatabaseTypeName(index int) string { + if typeName, ok := mysqlTypeMap[rows.rs.columns[index].fieldType]; ok { + return typeName + } + return FieldTypeUnknown +}