mysql: added support for tables in mysql queries

This commit is contained in:
Torkel Ödegaard
2017-04-21 15:07:43 +02:00
parent c78c460f79
commit 97e2d75f51
7 changed files with 201 additions and 40 deletions

View File

@ -51,14 +51,25 @@ type QueryResult struct {
RefId string `json:"refId"` RefId string `json:"refId"`
Meta *simplejson.Json `json:"meta,omitempty"` Meta *simplejson.Json `json:"meta,omitempty"`
Series TimeSeriesSlice `json:"series"` Series TimeSeriesSlice `json:"series"`
Tables []*Table `json:"tables"`
} }
type TimeSeries struct { type TimeSeries struct {
Name string `json:"name"` Name string `json:"name"`
Points TimeSeriesPoints `json:"points"` 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 TimePoint [2]null.Float
type TimeSeriesPoints []TimePoint type TimeSeriesPoints []TimePoint
type TimeSeriesSlice []*TimeSeries type TimeSeriesSlice []*TimeSeries

View File

@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"sync" "sync"
"github.com/go-sql-driver/mysql"
"github.com/go-xorm/core" "github.com/go-xorm/core"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/components/null" "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") format := query.Model.Get("format").MustString("time_series")
if format == "time_series" { switch format {
res, err := e.TransformToTimeSeries(query, rows) case "time_series":
err := e.TransformToTimeSeries(query, rows, queryResult)
if err != nil { if err != nil {
queryResult.Error = err 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 return result
} }
func countPointsInAllSeries(seriesList tsdb.TimeSeriesSlice) (count int) { func (e MysqlExecutor) TransformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
for _, series := range seriesList { columnNames, err := rows.Columns()
count += len(series.Points) 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) pointsBySeries := make(map[string]*tsdb.TimeSeries)
columnNames, err := rows.Columns() columnNames, err := rows.Columns()
if err != nil { if err != nil {
return nil, err return err
} }
rowData := NewStringStringScan(columnNames) rowData := NewStringStringScan(columnNames)
@ -150,13 +214,13 @@ func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows)
for ; rows.Next(); rowCount += 1 { for ; rows.Next(); rowCount += 1 {
if rowCount > rowLimit { 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) err := rowData.Update(rows.Rows)
if err != nil { if err != nil {
e.log.Error("MySQL response parsing", "error", err) 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 == "" { 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) //e.log.Debug("Rows", "metric", rowData.metric, "time", rowData.time, "value", rowData.value)
if !rowData.time.Valid { 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 { 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 { for _, value := range pointsBySeries {
seriesList = append(seriesList, value) result.Series = append(result.Series, value)
} }
e.log.Debug("TransformToTimeSeries", "rowCount", rowCount, "timeSeriesCount", len(seriesList)) result.Meta.Set("rowCount", rowCount)
return seriesList, nil return nil
} }
type stringStringScan struct { type stringStringScan struct {

View File

@ -31,6 +31,7 @@ class MetricsPanelCtrl extends PanelCtrl {
skipDataOnInit: boolean; skipDataOnInit: boolean;
dataStream: any; dataStream: any;
dataSubscription: any; dataSubscription: any;
dataList: any;
constructor($scope, $injector) { constructor($scope, $injector) {
super($scope, $injector); super($scope, $injector);

View File

@ -38,14 +38,20 @@ export class MysqlDatasource {
to: options.range.to.valueOf().toString(), to: options.range.to.valueOf().toString(),
queries: queries, queries: queries,
} }
}).then(res => { }).then(this.processQueryResult.bind(this));
var data = []; }
if (!res.data.results) { processQueryResult(res) {
return {data: data}; 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) { for (let series of queryRes.series) {
data.push({ data.push({
target: series.name, target: series.name,
@ -54,10 +60,19 @@ export class MysqlDatasource {
meta: queryRes.meta, 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};
} }
} }

View File

@ -27,7 +27,7 @@ class MysqlQueryCtrl extends QueryCtrl {
constructor($scope, $injector) { constructor($scope, $injector) {
super($scope, $injector); super($scope, $injector);
this.target.format = 'time_series'; this.target.format = this.target.format || 'time_series';
this.target.alias = ""; this.target.alias = "";
this.formats = [ this.formats = [
{text: 'Time series', value: 'time_series'}, {text: 'Time series', value: 'time_series'},

View File

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

View File

@ -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
}