diff --git a/pkg/tsdb/druid/druid.go b/pkg/tsdb/druid/druid.go index 91a50ca7a386c..27e00f3bdc005 100644 --- a/pkg/tsdb/druid/druid.go +++ b/pkg/tsdb/druid/druid.go @@ -486,7 +486,12 @@ func (ds *Service) executeQuery( } resultFramer = &r case "groupBy": - return nil, errors.New("not implemented") + var r result.GroupByResult + _, err := s.client.Query().Execute(q, &r, headers) + if err != nil { + return nil, fmt.Errorf("Query error: %w", err) + } + resultFramer = &r case "scan": q.(*druidquery.Scan).SetResultFormat("compactedList") return nil, errors.New("not implemented") diff --git a/pkg/tsdb/druid/result/groupby.go b/pkg/tsdb/druid/result/groupby.go new file mode 100644 index 0000000000000..e3cb2a49b9472 --- /dev/null +++ b/pkg/tsdb/druid/result/groupby.go @@ -0,0 +1,74 @@ +package result + +import ( + "sort" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +type GroupByResult []GroupByRecord + +// Frame returns data formatted as Grafana Frame. +func (t *GroupByResult) Frame() *data.Frame { + columns := t.Columns() + fields := make([]*data.Field, len(columns)) + for i, column := range columns { + labels := data.Labels{} + fields[i] = data.NewField(column, labels, t.Values(column)) + } + return data.NewFrame("", fields...) +} + +// Columns returns list of columns. It calls `Columns()` on first record. If +// no records are available it returns nil. +func (t *GroupByResult) Columns() []string { + for _, r := range *t { + return r.Columns() + } + return nil +} + +// Values returns all values for given column. +func (t *GroupByResult) Values(column string) interface{} { + if len(*t) == 0 { + return nil + } + results := make([]interface{}, len(*t)) + for i, r := range *t { + results[i] = r.Value(column) + } + return toTypedResults(results) +} + +type GroupByRecord struct { + Timestamp time.Time `json:"timestamp"` + Event map[string]interface{} `json:"event"` +} + +// Columns returns list of columns for given record. +// The first column will always be "timestamp" followed by other columns sorted +// alphabetically. +func (t *GroupByRecord) Columns() []string { + columns := make([]string, len(t.Event)+1) + columns[0] = timestampColumn + i := 1 + for c := range t.Event { + columns[i] = c + i++ + } + sort.Strings(columns[1:]) + return columns +} + +// Value returns value for given column. +func (t *GroupByRecord) Value(column string) interface{} { + if column == timestampColumn { + return t.Timestamp + } + v, ok := t.Event[column] + if !ok { + return nil + } + return v +} diff --git a/pkg/tsdb/druid/result/groupby_test.go b/pkg/tsdb/druid/result/groupby_test.go new file mode 100644 index 0000000000000..80cc4cb335b67 --- /dev/null +++ b/pkg/tsdb/druid/result/groupby_test.go @@ -0,0 +1,60 @@ +package result + +import ( + "encoding/json" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/assert" +) + +func TestGroupByResultUnmarshal(t *testing.T) { + input := []byte(`[ + { + "timestamp": "2022-10-14T08:08:10.000Z", + "event": { + "dog_count": 47, + "dog_rate": 2.083, + "dog_name": "foo" + } + }, + { + "timestamp": "2022-10-14T08:08:11.000Z", + "event": { + "dog_count": 75, + "dog_rate": 3.846, + "dog_name": "bar" + } + } + ]`) + + var res GroupByResult + err := json.Unmarshal(input, &res) + assert.Nil(t, err, "Failed to unmarshal response") + assert.Equal(t, len(res), 2, "Wrong number of unmarshalled results") + frame := res.Frame() + assert.Equal(t, len(frame.Fields), 4, "Wrong number of framed fields") + + assert.Equal(t, frame.Fields[0].Name, "timestamp") + assert.Equal(t, frame.Fields[0].Type(), data.FieldTypeTime) + assert.Equal(t, frame.Fields[0].Len(), 2) + assert.Equal(t, frame.Fields[0].At(0), time.Time(time.Date(2022, time.October, 14, 8, 8, 10, 0, time.UTC))) + assert.Equal(t, frame.Fields[0].At(1), time.Time(time.Date(2022, time.October, 14, 8, 8, 11, 0, time.UTC))) + + assert.Equal(t, frame.Fields[1].Name, "dog_count") + assert.Equal(t, frame.Fields[1].Type(), data.FieldTypeFloat64) + assert.Equal(t, frame.Fields[1].At(0), float64(47)) + assert.Equal(t, frame.Fields[1].At(1), float64(75)) + + assert.Equal(t, frame.Fields[2].Name, "dog_name") + assert.Equal(t, frame.Fields[2].Type(), data.FieldTypeString) + assert.Equal(t, frame.Fields[2].At(0), "foo") + assert.Equal(t, frame.Fields[2].At(1), "bar") + + assert.Equal(t, frame.Fields[3].Name, "dog_rate") + assert.Equal(t, frame.Fields[3].Type(), data.FieldTypeFloat64) + assert.Equal(t, frame.Fields[3].At(0), float64(2.083)) + assert.Equal(t, frame.Fields[3].At(1), float64(3.846)) + +} diff --git a/pkg/tsdb/druid/result/topn.go b/pkg/tsdb/druid/result/topn.go index 035dc23b4f67e..ba64fa7dc508b 100644 --- a/pkg/tsdb/druid/result/topn.go +++ b/pkg/tsdb/druid/result/topn.go @@ -46,7 +46,7 @@ type TopNRecord struct { // Columns returns list of columns for given record. // It assumes that every map from Result has the same columns, so it gets // the list from first item. -// The first column will always be "timestamp" followed by other columns sorter +// The first column will always be "timestamp" followed by other columns sorted // alphabetically. func (t *TopNRecord) Columns() []string { for _, result := range t.Result {