diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index abf208862c8..d0d4697c941 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -1163,6 +1163,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 := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT 1631053772276 as time, 1631054012276 as timeend, '' as text, '' as tags", + "format": "table" + }`), + RefID: "A", + }, + }, + } + + resp, err := endpoint.QueryData(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + 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 dc763afb43b..c414329e502 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -1125,6 +1125,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 := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT 1631053772276 as time, 1631054012276 as timeend, '' as text, '' as tags", + "format": "table" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryData(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + 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 37a22c22bbf..46273d66c28 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -1199,6 +1199,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 := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + JSON: []byte(`{ + "rawSql": "SELECT 1631053772276 as time, 1631054012276 as timeend, '' as text, '' as tags", + "format": "table" + }`), + RefID: "A", + }, + }, + } + + resp, err := exe.QueryData(context.Background(), query) + require.NoError(t, err) + queryResult := resp.Responses["A"] + require.NoError(t, queryResult.Error) + + frames := queryResult.Frames + 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 53709246608..20894613592 100644 --- a/pkg/tsdb/sqleng/sql_engine.go +++ b/pkg/tsdb/sqleng/sql_engine.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/tsdb/intervalv2" + "github.com/grafana/grafana/pkg/util/errutil" "xorm.io/core" "xorm.io/xorm" ) @@ -300,11 +301,9 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG 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 { @@ -408,6 +407,7 @@ func (e *DataSourceHandler) newProcessCfg(query backend.DataQuery, queryContext columnNames: columnNames, rows: rows, timeIndex: -1, + timeEndIndex: -1, metricIndex: -1, metricPrefix: false, queryContext: queryContext, @@ -454,6 +454,12 @@ func (e *DataSourceHandler) newProcessCfg(query backend.DataQuery, queryContext break } } + + if qm.Format == dataQueryFormatTable && col == "timeend" { + qm.timeEndIndex = i + continue + } + switch col { case "metric": qm.metricIndex = i @@ -492,6 +498,7 @@ type dataQueryModel struct { columnNames []string columnTypes []*sql.ColumnType timeIndex int + timeEndIndex int metricIndex int rows *core.Rows metricPrefix bool @@ -821,6 +828,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 {