From 4f215e06f13d2440b31e8834fc36489bb173cced Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 8 Sep 2021 08:51:27 +0200 Subject: [PATCH] Postgres/MySQL/MSSQL: Fix region annotations not displayed correctly (#38936) (#38953) Fix region annotations not displayed correctly when returning timeend column as epoch timestamp and by that making sure that the returned data frame field named timeend is treated as time type. Fixes #38533 (cherry picked from commit fbdaf56a846bfb48438c11e79502c4a0b4ac4f4d) --- pkg/tsdb/mssql/mssql_test.go | 32 ++++++++++++++++++++++++++--- pkg/tsdb/mysql/mysql_test.go | 26 +++++++++++++++++++++++ pkg/tsdb/postgres/postgres_test.go | 26 +++++++++++++++++++++++ pkg/tsdb/sqleng/sql_engine.go | 33 +++++++++++++++++++++++++----- 4 files changed, 109 insertions(+), 8 deletions(-) diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index b13ff27f4ea..ce5866847ce 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -151,7 +151,7 @@ func TestMSSQL(t *testing.T) { require.Equal(t, 1, len(frames)) require.Equal(t, 24, len(frames[0].Fields)) - require.Equal(t, true, frames[0].Fields[0].At(0).(bool)) + require.Equal(t, true, *frames[0].Fields[0].At(0).(*bool)) require.Equal(t, int64(5), *frames[0].Fields[1].At(0).(*int64)) require.Equal(t, int64(20020), *frames[0].Fields[2].At(0).(*int64)) require.Equal(t, int64(980300), *frames[0].Fields[3].At(0).(*int64)) @@ -654,8 +654,8 @@ func TestMSSQL(t *testing.T) { require.Equal(t, 1, len(frames)) require.Equal(t, 3, len(frames[0].Fields)) - require.Equal(t, data.Labels{"metric": "Metric A - value one"}, frames[0].Fields[1].Labels) - require.Equal(t, data.Labels{"metric": "Metric B - value one"}, frames[0].Fields[2].Labels) + require.Equal(t, "Metric A - value one", frames[0].Fields[1].Name) + require.Equal(t, "Metric B - value one", frames[0].Fields[2].Name) }) t.Run("When doing a metric query grouping by time should return correct series", func(t *testing.T) { @@ -1203,6 +1203,32 @@ func TestMSSQL(t *testing.T) { // Should be in time.Time require.Nil(t, frames[0].Fields[0].At(0)) }) + + t.Run("When doing an annotation query with a time and timeend column should return two fields of type time", func(t *testing.T) { + query := plugins.DataQuery{ + Queries: []plugins.DataSubQuery{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT 1631053772276 as time, 1631054012276 as timeend, '' as text, '' as tags", + "format": "table", + }), + RefID: "A", + }, + }, + } + + resp, err := endpoint.DataQuery(context.Background(), nil, query) + require.NoError(t, err) + queryResult := resp.Results["A"] + require.NoError(t, queryResult.Error) + + frames, _ := queryResult.Dataframes.Decoded() + require.Equal(t, 1, len(frames)) + require.Equal(t, 4, len(frames[0].Fields)) + + require.Equal(t, data.FieldTypeNullableTime, frames[0].Fields[0].Type()) + require.Equal(t, data.FieldTypeNullableTime, frames[0].Fields[1].Type()) + }) }) } diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index 2ffccddfece..2cc2815d152 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -1147,6 +1147,32 @@ func TestMySQL(t *testing.T) { //Should be in time.Time require.Nil(t, frames[0].Fields[0].At(0)) }) + + t.Run("When doing an annotation query with a time and timeend column should return two fields of type time", func(t *testing.T) { + query := plugins.DataQuery{ + Queries: []plugins.DataSubQuery{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT 1631053772276 as time, 1631054012276 as timeend, '' as text, '' as tags", + "format": "table", + }), + RefID: "A", + }, + }, + } + + resp, err := exe.DataQuery(context.Background(), nil, query) + require.NoError(t, err) + queryResult := resp.Results["A"] + require.NoError(t, queryResult.Error) + + frames, _ := queryResult.Dataframes.Decoded() + require.Equal(t, 1, len(frames)) + require.Equal(t, 4, len(frames[0].Fields)) + + require.Equal(t, data.FieldTypeNullableTime, frames[0].Fields[0].Type()) + require.Equal(t, data.FieldTypeNullableTime, frames[0].Fields[1].Type()) + }) }) } diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index 79425cafdc5..5d68de3b36d 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -1217,6 +1217,32 @@ func TestPostgres(t *testing.T) { // Should be in time.Time assert.Nil(t, frames[0].Fields[0].At(0)) }) + + t.Run("When doing an annotation query with a time and timeend column should return two fields of type time", func(t *testing.T) { + query := plugins.DataQuery{ + Queries: []plugins.DataSubQuery{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT 1631053772276 as time, 1631054012276 as timeend, '' as text, '' as tags", + "format": "table", + }), + RefID: "A", + }, + }, + } + + resp, err := exe.DataQuery(context.Background(), nil, query) + require.NoError(t, err) + queryResult := resp.Results["A"] + require.NoError(t, queryResult.Error) + + frames, _ := queryResult.Dataframes.Decoded() + require.Equal(t, 1, len(frames)) + require.Equal(t, 4, len(frames[0].Fields)) + + require.Equal(t, data.FieldTypeNullableTime, frames[0].Fields[0].Type()) + require.Equal(t, data.FieldTypeNullableTime, frames[0].Fields[1].Type()) + }) }) } diff --git a/pkg/tsdb/sqleng/sql_engine.go b/pkg/tsdb/sqleng/sql_engine.go index fed7612867e..94a098c0aaa 100644 --- a/pkg/tsdb/sqleng/sql_engine.go +++ b/pkg/tsdb/sqleng/sql_engine.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/tsdb/interval" + "github.com/grafana/grafana/pkg/util/errutil" "xorm.io/core" "xorm.io/xorm" ) @@ -270,11 +271,9 @@ func (e *dataPlugin) executeQuery(query plugins.DataSubQuery, wg *sync.WaitGroup return } - if qm.timeIndex != -1 { - if err := convertSQLTimeColumnToEpochMS(frame, qm.timeIndex); err != nil { - errAppendDebug("db convert time column failed", err, interpolatedQuery) - return - } + if err := convertSQLTimeColumnsToEpochMS(frame, qm); err != nil { + errAppendDebug("converting time columns failed", err, interpolatedQuery) + return } if qm.Format == dataQueryFormatSeries { @@ -375,6 +374,7 @@ func (e *dataPlugin) newProcessCfg(query plugins.DataSubQuery, queryContext plug columnNames: columnNames, rows: rows, timeIndex: -1, + timeEndIndex: -1, metricIndex: -1, metricPrefix: false, queryContext: queryContext, @@ -418,6 +418,12 @@ func (e *dataPlugin) newProcessCfg(query plugins.DataSubQuery, queryContext plug break } } + + if qm.Format == dataQueryFormatTable && col == "timeend" { + qm.timeEndIndex = i + continue + } + switch col { case "metric": qm.metricIndex = i @@ -456,6 +462,7 @@ type dataQueryModel struct { columnNames []string columnTypes []*sql.ColumnType timeIndex int + timeEndIndex int metricIndex int rows *core.Rows metricPrefix bool @@ -785,6 +792,22 @@ func convertNullableFloat32ToEpochMS(origin *data.Field, newField *data.Field) { } } +func convertSQLTimeColumnsToEpochMS(frame *data.Frame, qm *dataQueryModel) error { + if qm.timeIndex != -1 { + if err := convertSQLTimeColumnToEpochMS(frame, qm.timeIndex); err != nil { + return errutil.Wrap("failed to convert time column", err) + } + } + + if qm.timeEndIndex != -1 { + if err := convertSQLTimeColumnToEpochMS(frame, qm.timeEndIndex); err != nil { + return errutil.Wrap("failed to convert timeend column", err) + } + } + + return nil +} + // convertSQLTimeColumnToEpochMS converts column named time to unix timestamp in milliseconds // to make native datetime types and epoch dates work in annotation and table queries. func convertSQLTimeColumnToEpochMS(frame *data.Frame, timeIndex int) error {