Browse Source

mysql: added support for tables in mysql queries

Torkel Ödegaard 8 years ago
parent
commit
97e2d75f51

+ 12 - 1
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

+ 82 - 19
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) 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) (tsdb.TimeSeriesSlice, error) {
+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 {

+ 1 - 0
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);

+ 24 - 9
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};
   }
 }
 

+ 1 - 1
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'},

+ 3 - 10
public/app/plugins/datasource/mysql/partials/query.editor.html

@@ -16,24 +16,17 @@
 			<label class="gf-form-label query-keyword">Name by</label>
 			<input type="text" class="gf-form-input" ng-model="ctrl.target.alias" spellcheck='false' placeholder="pattern" ng-blur="ctrl.refresh()">
 		</div>
-
-		<div class="gf-form gf-form--grow">
-			<div class="gf-form-label gf-form-label--grow"></div>
-		</div>
-	</div>
-
-  <div class="gf-form-inline" ng-show="ctrl.lastQueryMeta">
-    <div class="gf-form">
+		<div class="gf-form" ng-show="ctrl.lastQueryMeta">
       <label class="gf-form-label query-keyword" ng-click="ctrl.showLastQuerySQL = !ctrl.showLastQuerySQL">
         Generated SQL
         <i class="fa fa-caret-down" ng-show="ctrl.showLastQuerySQL"></i>
         <i class="fa fa-caret-right" ng-hide="ctrl.showLastQuerySQL"></i>
       </label>
 		</div>
-    <div class="gf-form gf-form--grow">
+		<div class="gf-form gf-form--grow">
 			<div class="gf-form-label gf-form-label--grow"></div>
 		</div>
-  </div>
+	</div>
 
   <pre class="small" ng-show="ctrl.showLastQuerySQL">{{ctrl.lastQueryMeta.sql}}</pre>
   <pre class="small alert alert-error" ng-show="ctrl.lastQueryError">{{ctrl.lastQueryError}}</pre>

+ 78 - 0
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
+}