mirror of https://github.com/grafana/grafana
SQL Expressions: Change metric conversion to full long (#102728)
When querying metric data (non-table data) with SQL Expressions, we need to convert the data to table format. This is alternative format which does not have the same issues with sparse data. There is now a __metric_name__ column and one __value__ column. Also a __display_name__ column if there is DisplayNameFromDS metadata. --------- Co-authored-by: Adam Simpson <adam@adamsimpson.net>pull/102751/head
parent
5dd0aa2c73
commit
f4849eabc7
@ -0,0 +1,128 @@ |
||||
package expr |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
) |
||||
|
||||
func ConvertFromFullLongToNumericMulti(frames data.Frames) (data.Frames, error) { |
||||
if len(frames) != 1 { |
||||
return nil, fmt.Errorf("expected exactly one frame, got %d", len(frames)) |
||||
} |
||||
frame := frames[0] |
||||
if frame.Meta == nil || frame.Meta.Type != numericFullLongType { |
||||
return nil, fmt.Errorf("expected frame of type %q", numericFullLongType) |
||||
} |
||||
|
||||
var ( |
||||
metricField *data.Field |
||||
valueField *data.Field |
||||
displayField *data.Field |
||||
labelFields []*data.Field |
||||
) |
||||
|
||||
// Identify key fields
|
||||
for _, f := range frame.Fields { |
||||
switch f.Name { |
||||
case SQLMetricFieldName: |
||||
metricField = f |
||||
case SQLValueFieldName: |
||||
valueField = f |
||||
case SQLDisplayFieldName: |
||||
displayField = f |
||||
default: |
||||
if f.Type() == data.FieldTypeNullableString { |
||||
labelFields = append(labelFields, f) |
||||
} |
||||
} |
||||
} |
||||
|
||||
if metricField == nil || valueField == nil { |
||||
return nil, fmt.Errorf("missing required fields: %q or %q", SQLMetricFieldName, SQLValueFieldName) |
||||
} |
||||
|
||||
type seriesKey struct { |
||||
metric string |
||||
labelFP data.Fingerprint |
||||
displayName string |
||||
} |
||||
|
||||
type seriesEntry struct { |
||||
indices []int |
||||
labels data.Labels |
||||
displayName *string |
||||
} |
||||
|
||||
grouped := make(map[seriesKey]*seriesEntry) |
||||
|
||||
for i := 0; i < frame.Rows(); i++ { |
||||
if valueField.NilAt(i) { |
||||
continue // skip null values
|
||||
} |
||||
|
||||
metric := metricField.At(i).(string) |
||||
|
||||
// collect labels
|
||||
labels := data.Labels{} |
||||
for _, f := range labelFields { |
||||
if f.NilAt(i) { |
||||
continue |
||||
} |
||||
val := f.At(i).(*string) |
||||
if val != nil { |
||||
labels[f.Name] = *val |
||||
} |
||||
} |
||||
fp := labels.Fingerprint() |
||||
|
||||
// handle optional display name
|
||||
var displayPtr *string |
||||
displayKey := "" |
||||
if displayField != nil && !displayField.NilAt(i) { |
||||
if raw := displayField.At(i).(*string); raw != nil { |
||||
displayPtr = raw |
||||
displayKey = *raw |
||||
} |
||||
} |
||||
|
||||
key := seriesKey{ |
||||
metric: metric, |
||||
labelFP: fp, |
||||
displayName: displayKey, |
||||
} |
||||
|
||||
entry, ok := grouped[key] |
||||
if !ok { |
||||
entry = &seriesEntry{ |
||||
labels: labels, |
||||
displayName: displayPtr, |
||||
} |
||||
grouped[key] = entry |
||||
} |
||||
entry.indices = append(entry.indices, i) |
||||
} |
||||
|
||||
var result data.Frames |
||||
for key, entry := range grouped { |
||||
values := make([]*float64, 0, len(entry.indices)) |
||||
for _, i := range entry.indices { |
||||
v, err := valueField.FloatAt(i) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to convert value at index %d to float: %w", i, err) |
||||
} |
||||
values = append(values, &v) |
||||
} |
||||
|
||||
field := data.NewField(key.metric, entry.labels, values) |
||||
if entry.displayName != nil { |
||||
field.Config = &data.FieldConfig{DisplayNameFromDS: *entry.displayName} |
||||
} |
||||
|
||||
frame := data.NewFrame("", field) |
||||
frame.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} |
||||
result = append(result, frame) |
||||
} |
||||
|
||||
return result, nil |
||||
} |
@ -0,0 +1,192 @@ |
||||
package expr |
||||
|
||||
import ( |
||||
"sort" |
||||
"testing" |
||||
|
||||
"github.com/google/go-cmp/cmp" |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestConvertFromFullLongToNumericMulti(t *testing.T) { |
||||
t.Run("SingleRowNoLabels", func(t *testing.T) { |
||||
input := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(3.14)}), |
||||
) |
||||
input.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
out, err := ConvertFromFullLongToNumericMulti(data.Frames{input}) |
||||
require.NoError(t, err) |
||||
require.Len(t, out, 1) |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("cpu", nil, []*float64{fp(3.14)}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} |
||||
|
||||
if diff := cmp.Diff(expected, out[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoRowsWithLabelsAndDisplay", func(t *testing.T) { |
||||
input := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), |
||||
data.NewField(SQLDisplayFieldName, nil, []*string{sp("CPU A"), sp("CPU A")}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("a")}), |
||||
) |
||||
input.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
out, err := ConvertFromFullLongToNumericMulti(data.Frames{input}) |
||||
require.NoError(t, err) |
||||
require.Len(t, out, 1) |
||||
|
||||
expected := data.NewFrame("", |
||||
func() *data.Field { |
||||
f := data.NewField("cpu", data.Labels{"host": "a"}, []*float64{fp(1.0), fp(2.0)}) |
||||
f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU A"} |
||||
return f |
||||
}(), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} |
||||
|
||||
if diff := cmp.Diff(expected, out[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("SkipsNullValues", func(t *testing.T) { |
||||
input := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), nil}), |
||||
) |
||||
input.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
out, err := ConvertFromFullLongToNumericMulti(data.Frames{input}) |
||||
require.NoError(t, err) |
||||
require.Len(t, out, 1) |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("cpu", nil, []*float64{fp(1.0)}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} |
||||
|
||||
if diff := cmp.Diff(expected, out[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func TestConvertNumericMultiRoundTripToFullLongAndBack(t *testing.T) { |
||||
t.Run("TwoFieldsWithSparseLabels", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("", |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []*float64{fp(1.0)}), |
||||
), |
||||
data.NewFrame("", |
||||
data.NewField("cpu", data.Labels{"host": "b", "env": "prod"}, []*float64{fp(2.0)}), |
||||
), |
||||
} |
||||
for _, f := range input { |
||||
f.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} |
||||
} |
||||
|
||||
fullLong, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, fullLong, 1) |
||||
|
||||
roundTrip, err := ConvertFromFullLongToNumericMulti(fullLong) |
||||
require.NoError(t, err) |
||||
|
||||
expected := data.Frames{ |
||||
data.NewFrame("", |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []*float64{fp(1.0)}), |
||||
), |
||||
data.NewFrame("", |
||||
data.NewField("cpu", data.Labels{"host": "b", "env": "prod"}, []*float64{fp(2.0)}), |
||||
), |
||||
} |
||||
for _, f := range expected { |
||||
f.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} |
||||
} |
||||
|
||||
sortFramesByMetricDisplayAndLabels(expected) |
||||
sortFramesByMetricDisplayAndLabels(roundTrip) |
||||
|
||||
require.Len(t, roundTrip, len(expected)) |
||||
for i := range expected { |
||||
if diff := cmp.Diff(expected[i], roundTrip[i], data.FrameTestCompareOptions()...); diff != "" { |
||||
t.Errorf("Mismatch on frame %d (-want +got):\n%s", i, diff) |
||||
} |
||||
} |
||||
}) |
||||
|
||||
t.Run("PreservesDisplayName", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("", |
||||
func() *data.Field { |
||||
f := data.NewField("cpu", data.Labels{"host": "a"}, []*float64{fp(1.0)}) |
||||
f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU A"} |
||||
return f |
||||
}(), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} |
||||
|
||||
fullLong, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, fullLong, 1) |
||||
|
||||
roundTrip, err := ConvertFromFullLongToNumericMulti(fullLong) |
||||
require.NoError(t, err) |
||||
|
||||
expected := data.Frames{ |
||||
data.NewFrame("", |
||||
func() *data.Field { |
||||
f := data.NewField("cpu", data.Labels{"host": "a"}, []*float64{fp(1.0)}) |
||||
f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU A"} |
||||
return f |
||||
}(), |
||||
), |
||||
} |
||||
expected[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} |
||||
|
||||
sortFramesByMetricDisplayAndLabels(expected) |
||||
sortFramesByMetricDisplayAndLabels(roundTrip) |
||||
|
||||
require.Len(t, roundTrip, 1) |
||||
if diff := cmp.Diff(expected[0], roundTrip[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
t.Errorf("Mismatch (-want +got):\n%s", diff) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func sortFramesByMetricDisplayAndLabels(frames data.Frames) { |
||||
sort.Slice(frames, func(i, j int) bool { |
||||
fi := frames[i].Fields[0] |
||||
fj := frames[j].Fields[0] |
||||
|
||||
// 1. Metric name
|
||||
if fi.Name != fj.Name { |
||||
return fi.Name < fj.Name |
||||
} |
||||
|
||||
// 2. Display name (if set)
|
||||
var di, dj string |
||||
if fi.Config != nil { |
||||
di = fi.Config.DisplayNameFromDS |
||||
} |
||||
if fj.Config != nil { |
||||
dj = fj.Config.DisplayNameFromDS |
||||
} |
||||
if di != dj { |
||||
return di < dj |
||||
} |
||||
|
||||
// 3. Labels fingerprint
|
||||
return fi.Labels.Fingerprint() < fj.Labels.Fingerprint() |
||||
}) |
||||
} |
@ -0,0 +1,400 @@ |
||||
package expr |
||||
|
||||
import ( |
||||
"fmt" |
||||
"sort" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
) |
||||
|
||||
const ( |
||||
SQLMetricFieldName = "__metric_name__" |
||||
SQLValueFieldName = "__value__" |
||||
SQLDisplayFieldName = "__display_name__" |
||||
|
||||
// These are not types in the SDK or dataplane contract yet.
|
||||
numericFullLongType = "numeric_full_long" |
||||
timeseriesFullLongType = "time_series_full_long" |
||||
) |
||||
|
||||
func ConvertToFullLong(frames data.Frames) (data.Frames, error) { |
||||
if len(frames) == 0 { |
||||
return frames, nil |
||||
} |
||||
|
||||
var inputType data.FrameType |
||||
if frames[0].Meta != nil && frames[0].Meta.Type != "" { |
||||
inputType = frames[0].Meta.Type |
||||
} else { |
||||
return nil, fmt.Errorf("input frame missing FrameMeta.Type") |
||||
} |
||||
|
||||
if !supportedToLongConversion(inputType) { |
||||
return nil, fmt.Errorf("unsupported input dataframe type %s for full long conversion", inputType) |
||||
} |
||||
|
||||
switch inputType { |
||||
case data.FrameTypeNumericMulti: |
||||
return convertNumericMultiToFullLong(frames) |
||||
case data.FrameTypeNumericWide: |
||||
return convertNumericWideToFullLong(frames) |
||||
case data.FrameTypeTimeSeriesMulti: |
||||
return convertTimeSeriesMultiToFullLong(frames) |
||||
case data.FrameTypeTimeSeriesWide: |
||||
return convertTimeSeriesWideToFullLong(frames) |
||||
default: |
||||
return nil, fmt.Errorf("unsupported input type %s for full long conversion", inputType) |
||||
} |
||||
} |
||||
|
||||
func convertNumericMultiToFullLong(frames data.Frames) (data.Frames, error) { |
||||
wide := convertNumericMultiToNumericWide(frames) |
||||
return convertNumericWideToFullLong(wide) |
||||
} |
||||
|
||||
func convertNumericWideToFullLong(frames data.Frames) (data.Frames, error) { |
||||
if len(frames) != 1 { |
||||
return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames)) |
||||
} |
||||
inputFrame := frames[0] |
||||
if inputFrame.Rows() > 1 { |
||||
return nil, fmt.Errorf("expected no more than one row in the frame, but got %d", inputFrame.Rows()) |
||||
} |
||||
|
||||
var ( |
||||
metricCol = make([]string, 0, len(inputFrame.Fields)) |
||||
valueCol = make([]*float64, 0, len(inputFrame.Fields)) |
||||
displayCol = make([]*string, 0, len(inputFrame.Fields)) |
||||
hasDisplayCol bool |
||||
) |
||||
|
||||
labelKeySet := map[string]struct{}{} |
||||
for _, field := range inputFrame.Fields { |
||||
if !field.Type().Numeric() { |
||||
continue |
||||
} |
||||
val, err := field.FloatAt(0) |
||||
if err != nil { |
||||
continue |
||||
} |
||||
v := val |
||||
valueCol = append(valueCol, &v) |
||||
metricCol = append(metricCol, field.Name) |
||||
|
||||
// Display name
|
||||
var d *string |
||||
if field.Config != nil && field.Config.DisplayNameFromDS != "" { |
||||
s := field.Config.DisplayNameFromDS |
||||
d = &s |
||||
hasDisplayCol = true |
||||
} |
||||
displayCol = append(displayCol, d) |
||||
|
||||
for k := range field.Labels { |
||||
labelKeySet[k] = struct{}{} |
||||
} |
||||
} |
||||
|
||||
labelKeys := make([]string, 0, len(labelKeySet)) |
||||
|
||||
labelValues := make(map[string][]*string) |
||||
for k := range labelKeySet { |
||||
labelKeys = append(labelKeys, k) |
||||
labelValues[k] = make([]*string, 0, len(valueCol)) |
||||
} |
||||
sort.Strings(labelKeys) |
||||
|
||||
for _, field := range inputFrame.Fields { |
||||
if !field.Type().Numeric() { |
||||
continue |
||||
} |
||||
for _, k := range labelKeys { |
||||
var val *string |
||||
if field.Labels != nil { |
||||
if v, ok := field.Labels[k]; ok { |
||||
val = &v |
||||
} |
||||
} |
||||
labelValues[k] = append(labelValues[k], val) |
||||
} |
||||
} |
||||
|
||||
fields := []*data.Field{ |
||||
data.NewField(SQLMetricFieldName, nil, metricCol), |
||||
data.NewField(SQLValueFieldName, nil, valueCol), |
||||
} |
||||
if hasDisplayCol { |
||||
fields = append(fields, data.NewField(SQLDisplayFieldName, nil, displayCol)) |
||||
} |
||||
for _, k := range labelKeys { |
||||
fields = append(fields, data.NewField(k, nil, labelValues[k])) |
||||
} |
||||
|
||||
out := data.NewFrame("", fields...) |
||||
out.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
return data.Frames{out}, nil |
||||
} |
||||
|
||||
func convertTimeSeriesMultiToFullLong(frames data.Frames) (data.Frames, error) { |
||||
type row struct { |
||||
t time.Time |
||||
value *float64 |
||||
metric string |
||||
display *string |
||||
labels data.Labels |
||||
} |
||||
|
||||
var rows []row |
||||
labelKeysSet := map[string]struct{}{} |
||||
hasDisplayCol := false |
||||
|
||||
for _, frame := range frames { |
||||
var timeField *data.Field |
||||
for _, f := range frame.Fields { |
||||
if f.Type() == data.FieldTypeTime { |
||||
timeField = f |
||||
break |
||||
} |
||||
} |
||||
if timeField == nil { |
||||
return nil, fmt.Errorf("missing time field") |
||||
} |
||||
for _, f := range frame.Fields { |
||||
if !f.Type().Numeric() { |
||||
continue |
||||
} |
||||
var display *string |
||||
if f.Config != nil && f.Config.DisplayNameFromDS != "" { |
||||
s := f.Config.DisplayNameFromDS |
||||
display = &s |
||||
hasDisplayCol = true |
||||
} |
||||
for i := 0; i < f.Len(); i++ { |
||||
t := timeField.At(i).(time.Time) |
||||
v, err := f.FloatAt(i) |
||||
if err != nil { |
||||
continue |
||||
} |
||||
val := v |
||||
rows = append(rows, row{ |
||||
t: t, |
||||
value: &val, |
||||
metric: f.Name, |
||||
display: display, |
||||
labels: f.Labels, |
||||
}) |
||||
for k := range f.Labels { |
||||
labelKeysSet[k] = struct{}{} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
labelKeys := make([]string, 0, len(labelKeysSet)) |
||||
for k := range labelKeysSet { |
||||
labelKeys = append(labelKeys, k) |
||||
} |
||||
sort.Strings(labelKeys) |
||||
sort.SliceStable(rows, func(i, j int) bool { |
||||
if rows[i].t.Equal(rows[j].t) { |
||||
return rows[i].metric < rows[j].metric |
||||
} |
||||
return rows[i].t.Before(rows[j].t) |
||||
}) |
||||
|
||||
times := make([]time.Time, len(rows)) |
||||
values := make([]*float64, len(rows)) |
||||
metrics := make([]string, len(rows)) |
||||
var displays []*string |
||||
if hasDisplayCol { |
||||
displays = make([]*string, len(rows)) |
||||
} |
||||
labels := make(map[string][]*string) |
||||
for _, k := range labelKeys { |
||||
labels[k] = make([]*string, len(rows)) |
||||
} |
||||
|
||||
for i, r := range rows { |
||||
times[i] = r.t |
||||
values[i] = r.value |
||||
metrics[i] = r.metric |
||||
if hasDisplayCol { |
||||
displays[i] = r.display |
||||
} |
||||
for _, k := range labelKeys { |
||||
if v, ok := r.labels[k]; ok { |
||||
labels[k][i] = &v |
||||
} |
||||
} |
||||
} |
||||
|
||||
fields := []*data.Field{ |
||||
data.NewField("time", nil, times), |
||||
data.NewField(SQLValueFieldName, nil, values), |
||||
data.NewField(SQLMetricFieldName, nil, metrics), |
||||
} |
||||
if hasDisplayCol { |
||||
fields = append(fields, data.NewField(SQLDisplayFieldName, nil, displays)) |
||||
} |
||||
for _, k := range labelKeys { |
||||
fields = append(fields, data.NewField(k, nil, labels[k])) |
||||
} |
||||
|
||||
out := data.NewFrame("", fields...) |
||||
out.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
return data.Frames{out}, nil |
||||
} |
||||
|
||||
func convertTimeSeriesWideToFullLong(frames data.Frames) (data.Frames, error) { |
||||
if len(frames) != 1 { |
||||
return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames)) |
||||
} |
||||
frame := frames[0] |
||||
|
||||
var timeField *data.Field |
||||
for _, f := range frame.Fields { |
||||
if f.Type() == data.FieldTypeTime { |
||||
timeField = f |
||||
break |
||||
} |
||||
} |
||||
if timeField == nil { |
||||
return nil, fmt.Errorf("time field not found in TimeSeriesWide frame") |
||||
} |
||||
|
||||
type row struct { |
||||
t time.Time |
||||
value *float64 |
||||
metric string |
||||
display *string |
||||
labels data.Labels |
||||
} |
||||
|
||||
var ( |
||||
rows []row |
||||
labelKeysSet = map[string]struct{}{} |
||||
hasDisplayCol bool |
||||
) |
||||
|
||||
// Collect all label keys
|
||||
for _, f := range frame.Fields { |
||||
if !f.Type().Numeric() { |
||||
continue |
||||
} |
||||
for k := range f.Labels { |
||||
labelKeysSet[k] = struct{}{} |
||||
} |
||||
} |
||||
|
||||
labelKeys := make([]string, 0, len(labelKeysSet)) |
||||
for k := range labelKeysSet { |
||||
labelKeys = append(labelKeys, k) |
||||
} |
||||
sort.Strings(labelKeys) |
||||
|
||||
timeLen := timeField.Len() |
||||
for _, f := range frame.Fields { |
||||
if !f.Type().Numeric() { |
||||
continue |
||||
} |
||||
var display *string |
||||
if f.Config != nil && f.Config.DisplayNameFromDS != "" { |
||||
s := f.Config.DisplayNameFromDS |
||||
display = &s |
||||
hasDisplayCol = true |
||||
} |
||||
for i := 0; i < timeLen; i++ { |
||||
t := timeField.At(i).(time.Time) |
||||
v, err := f.FloatAt(i) |
||||
if err != nil { |
||||
continue |
||||
} |
||||
val := v |
||||
rows = append(rows, row{ |
||||
t: t, |
||||
value: &val, |
||||
metric: f.Name, |
||||
display: display, |
||||
labels: f.Labels, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
sort.SliceStable(rows, func(i, j int) bool { |
||||
if rows[i].t.Equal(rows[j].t) { |
||||
return rows[i].metric < rows[j].metric |
||||
} |
||||
return rows[i].t.Before(rows[j].t) |
||||
}) |
||||
|
||||
times := make([]time.Time, len(rows)) |
||||
values := make([]*float64, len(rows)) |
||||
metrics := make([]string, len(rows)) |
||||
var displays []*string |
||||
if hasDisplayCol { |
||||
displays = make([]*string, len(rows)) |
||||
} |
||||
labels := make(map[string][]*string) |
||||
for _, k := range labelKeys { |
||||
labels[k] = make([]*string, len(rows)) |
||||
} |
||||
|
||||
for i, r := range rows { |
||||
times[i] = r.t |
||||
values[i] = r.value |
||||
metrics[i] = r.metric |
||||
if hasDisplayCol { |
||||
displays[i] = r.display |
||||
} |
||||
for _, k := range labelKeys { |
||||
if v, ok := r.labels[k]; ok { |
||||
labels[k][i] = &v |
||||
} |
||||
} |
||||
} |
||||
|
||||
fields := []*data.Field{ |
||||
data.NewField("time", nil, times), |
||||
data.NewField(SQLValueFieldName, nil, values), |
||||
data.NewField(SQLMetricFieldName, nil, metrics), |
||||
} |
||||
if hasDisplayCol { |
||||
fields = append(fields, data.NewField(SQLDisplayFieldName, nil, displays)) |
||||
} |
||||
for _, k := range labelKeys { |
||||
fields = append(fields, data.NewField(k, nil, labels[k])) |
||||
} |
||||
|
||||
out := data.NewFrame("", fields...) |
||||
out.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
return data.Frames{out}, nil |
||||
} |
||||
|
||||
func supportedToLongConversion(inputType data.FrameType) bool { |
||||
switch inputType { |
||||
case data.FrameTypeNumericMulti, data.FrameTypeNumericWide: |
||||
return true |
||||
case data.FrameTypeTimeSeriesMulti, data.FrameTypeTimeSeriesWide: |
||||
return true |
||||
default: |
||||
return false |
||||
} |
||||
} |
||||
|
||||
func convertNumericMultiToNumericWide(frames data.Frames) data.Frames { |
||||
if len(frames) == 0 { |
||||
return nil |
||||
} |
||||
|
||||
out := data.NewFrame("") |
||||
for _, frame := range frames { |
||||
for _, field := range frame.Fields { |
||||
if field.Type().Numeric() { |
||||
out.Fields = append(out.Fields, field) |
||||
} |
||||
} |
||||
} |
||||
out.Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} |
||||
return data.Frames{out} |
||||
} |
@ -0,0 +1,507 @@ |
||||
package expr |
||||
|
||||
import ( |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/google/go-cmp/cmp" |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestConvertNumericWideToFullLong(t *testing.T) { |
||||
t.Run("SingleItemNoLabels", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("numeric", |
||||
data.NewField("cpu", nil, []float64{3.14}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(3.14)}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("MultiRowShouldError", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("numeric", |
||||
data.NewField("cpu", nil, []float64{1.0, 2.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} |
||||
|
||||
_, err := ConvertToFullLong(input) |
||||
require.Error(t, err) |
||||
require.Contains(t, err.Error(), "no more than one row") |
||||
}) |
||||
|
||||
t.Run("TwoItemsWithSingleLabel", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("numeric", |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), |
||||
data.NewField("cpu", data.Labels{"host": "b"}, []float64{2.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("b")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoItemsWithSparseLabels", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("numeric", |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), |
||||
data.NewField("cpu", data.Labels{"host": "b", "env": "prod"}, []float64{2.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), |
||||
data.NewField("env", nil, []*string{nil, sp("prod")}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("b")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoDifferentMetricsWithSharedLabels", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("numeric", |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), |
||||
data.NewField("mem", data.Labels{"host": "a"}, []float64{4.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(4.0)}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("a")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoSparseMetricsAndLabels", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("numeric", |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), |
||||
data.NewField("mem", data.Labels{"env": "prod"}, []float64{4.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(4.0)}), |
||||
data.NewField("env", nil, []*string{nil, sp("prod")}), |
||||
data.NewField("host", nil, []*string{sp("a"), nil}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("ExtraTimeFieldIsDropped", func(t *testing.T) { |
||||
// Note we may consider changing this behavior and looking into keeping
|
||||
// remainder fields in the future.
|
||||
input := data.Frames{ |
||||
data.NewFrame("numeric", |
||||
data.NewField("timestamp", nil, []time.Time{time.Now()}), // extra time field
|
||||
data.NewField("cpu", nil, []float64{1.23}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.23)}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func TestConvertNumericWideToFullLongWithDisplayName(t *testing.T) { |
||||
t.Run("SingleFieldWithDisplayName", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("numeric", |
||||
func() *data.Field { |
||||
f := data.NewField("cpu", nil, []float64{3.14}) |
||||
f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU Display"} |
||||
return f |
||||
}(), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(3.14)}), |
||||
data.NewField(SQLDisplayFieldName, nil, []*string{sp("CPU Display")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("MixedDisplayNames", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("numeric", |
||||
func() *data.Field { |
||||
f := data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}) |
||||
f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU A"} |
||||
return f |
||||
}(), |
||||
data.NewField("cpu", data.Labels{"host": "b"}, []float64{2.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), |
||||
data.NewField(SQLDisplayFieldName, nil, []*string{sp("CPU A"), nil}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("b")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func TestConvertNumericMultiToFullLong(t *testing.T) { |
||||
t.Run("SingleItemNoLabels", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("", |
||||
data.NewField("cpu", nil, []float64{3.14}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(3.14)}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoItemsWithSingleLabel", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("", |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), |
||||
), |
||||
data.NewFrame("", |
||||
data.NewField("cpu", data.Labels{"host": "b"}, []float64{2.0}), |
||||
), |
||||
} |
||||
for _, f := range input { |
||||
f.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} |
||||
} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("b")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoItemsWithSparseLabels", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("", |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), |
||||
), |
||||
data.NewFrame("", |
||||
data.NewField("cpu", data.Labels{"host": "b", "env": "prod"}, []float64{2.0}), |
||||
), |
||||
} |
||||
for _, f := range input { |
||||
f.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} |
||||
} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), |
||||
data.NewField("env", nil, []*string{nil, sp("prod")}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("b")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoDifferentMetricsWithSharedLabels", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("", |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), |
||||
), |
||||
data.NewFrame("", |
||||
data.NewField("mem", data.Labels{"host": "a"}, []float64{4.0}), |
||||
), |
||||
} |
||||
for _, f := range input { |
||||
f.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} |
||||
} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(4.0)}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("a")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoSparseMetricsAndLabels", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("", |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0}), |
||||
), |
||||
data.NewFrame("", |
||||
data.NewField("mem", data.Labels{"env": "prod"}, []float64{4.0}), |
||||
), |
||||
} |
||||
for _, f := range input { |
||||
f.Meta = &data.FrameMeta{Type: data.FrameTypeNumericMulti} |
||||
} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem"}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(4.0)}), |
||||
data.NewField("env", nil, []*string{nil, sp("prod")}), |
||||
data.NewField("host", nil, []*string{sp("a"), nil}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: numericFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func TestConvertTimeSeriesWideToFullLong(t *testing.T) { |
||||
times := []time.Time{ |
||||
time.Unix(0, 0), |
||||
time.Unix(10, 0), |
||||
} |
||||
|
||||
t.Run("SingleSeriesNoLabels", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", nil, []float64{1.0, 2.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesWide} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, times), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoSeriesOneLabel", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), |
||||
data.NewField("cpu", data.Labels{"host": "b"}, []float64{3.0, 4.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesWide} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, []time.Time{times[0], times[0], times[1], times[1]}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu", "cpu", "cpu"}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoMetricsWithSharedLabels", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), |
||||
data.NewField("mem", data.Labels{"host": "a"}, []float64{3.0, 4.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesWide} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, []time.Time{times[0], times[0], times[1], times[1]}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem", "cpu", "mem"}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("a"), sp("a"), sp("a")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoSeriesSparseLabels", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), |
||||
data.NewField("cpu", data.Labels{"host": "b", "env": "prod"}, []float64{3.0, 4.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesWide} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, []time.Time{times[0], times[0], times[1], times[1]}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu", "cpu", "cpu"}), |
||||
data.NewField("env", nil, []*string{nil, sp("prod"), nil, sp("prod")}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoSeriesSparseMetricsAndLabels", func(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), |
||||
data.NewField("mem", data.Labels{"host": "b", "env": "prod"}, []float64{3.0, 4.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesWide} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, []time.Time{times[0], times[0], times[1], times[1]}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem", "cpu", "mem"}), |
||||
data.NewField("env", nil, []*string{nil, sp("prod"), nil, sp("prod")}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func sp(s string) *string { |
||||
return &s |
||||
} |
@ -0,0 +1,373 @@ |
||||
package expr |
||||
|
||||
import ( |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/google/go-cmp/cmp" |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestConvertTimeSeriesMultiToFullLong(t *testing.T) { |
||||
t.Run("SingleSeriesNoLabels", func(t *testing.T) { |
||||
times := []time.Time{ |
||||
time.Unix(0, 0), |
||||
time.Unix(10, 0), |
||||
time.Unix(20, 0), |
||||
} |
||||
values := []float64{1.0, 2.0, 3.0} |
||||
|
||||
input := data.Frames{ |
||||
data.NewFrame("cpu", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", nil, values), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, times), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0), fp(3.0)}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu", "cpu"}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoSeriesOneLabel", func(t *testing.T) { |
||||
times := []time.Time{ |
||||
time.Unix(0, 0), |
||||
time.Unix(10, 0), |
||||
} |
||||
|
||||
input := data.Frames{ |
||||
data.NewFrame("cpu", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), |
||||
), |
||||
data.NewFrame("cpu", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", data.Labels{"host": "b"}, []float64{3.0, 4.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
input[1].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, []time.Time{ |
||||
time.Unix(0, 0), time.Unix(0, 0), time.Unix(10, 0), time.Unix(10, 0), |
||||
}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu", "cpu", "cpu"}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoMetricsWithSharedLabels", func(t *testing.T) { |
||||
times := []time.Time{ |
||||
time.Unix(0, 0), |
||||
time.Unix(10, 0), |
||||
} |
||||
|
||||
input := data.Frames{ |
||||
data.NewFrame("cpu", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), |
||||
), |
||||
data.NewFrame("mem", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("mem", data.Labels{"host": "a"}, []float64{3.0, 4.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
input[1].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, []time.Time{ |
||||
time.Unix(0, 0), time.Unix(0, 0), time.Unix(10, 0), time.Unix(10, 0), |
||||
}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem", "cpu", "mem"}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("a"), sp("a"), sp("a")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoSeriesSparseLabels", func(t *testing.T) { |
||||
times := []time.Time{ |
||||
time.Unix(0, 0), |
||||
time.Unix(10, 0), |
||||
} |
||||
|
||||
input := data.Frames{ |
||||
data.NewFrame("cpu", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), |
||||
), |
||||
data.NewFrame("cpu", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", data.Labels{"host": "b", "env": "prod"}, []float64{3.0, 4.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
input[1].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, []time.Time{ |
||||
time.Unix(0, 0), time.Unix(0, 0), time.Unix(10, 0), time.Unix(10, 0), |
||||
}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu", "cpu", "cpu"}), |
||||
data.NewField("env", nil, []*string{nil, sp("prod"), nil, sp("prod")}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoSeriesSparseMetrics", func(t *testing.T) { |
||||
times := []time.Time{ |
||||
time.Unix(0, 0), |
||||
time.Unix(10, 0), |
||||
} |
||||
|
||||
input := data.Frames{ |
||||
data.NewFrame("cpu", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), |
||||
), |
||||
data.NewFrame("mem", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("mem", data.Labels{"host": "b"}, []float64{3.0, 4.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
input[1].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, []time.Time{ |
||||
time.Unix(0, 0), time.Unix(0, 0), time.Unix(10, 0), time.Unix(10, 0), |
||||
}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem", "cpu", "mem"}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoSeriesSparseMetricsAndLabels", func(t *testing.T) { |
||||
times := []time.Time{ |
||||
time.Unix(0, 0), |
||||
time.Unix(10, 0), |
||||
} |
||||
|
||||
input := data.Frames{ |
||||
data.NewFrame("cpu", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), |
||||
), |
||||
data.NewFrame("mem", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("mem", data.Labels{"host": "b", "env": "prod"}, []float64{3.0, 4.0}), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
input[1].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, []time.Time{ |
||||
time.Unix(0, 0), time.Unix(0, 0), time.Unix(10, 0), time.Unix(10, 0), |
||||
}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(3.0), fp(2.0), fp(4.0)}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "mem", "cpu", "mem"}), |
||||
data.NewField("env", nil, []*string{nil, sp("prod"), nil, sp("prod")}), |
||||
data.NewField("host", nil, []*string{sp("a"), sp("b"), sp("a"), sp("b")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("ThreeSeriesSparseTimeLabelsMetrics", func(t *testing.T) { |
||||
timesA := []time.Time{ |
||||
time.Unix(0, 0), |
||||
time.Unix(10, 0), |
||||
} |
||||
timesB := []time.Time{ |
||||
time.Unix(5, 0), |
||||
time.Unix(15, 0), |
||||
} |
||||
timesMem := []time.Time{ |
||||
time.Unix(10, 0), |
||||
time.Unix(30, 0), |
||||
} |
||||
|
||||
input := data.Frames{ |
||||
data.NewFrame("cpu", |
||||
data.NewField("time", nil, timesA), |
||||
data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}), |
||||
), |
||||
data.NewFrame("cpu", |
||||
data.NewField("time", nil, timesB), |
||||
data.NewField("cpu", nil, []float64{9.0, 10.0}), // no labels
|
||||
), |
||||
data.NewFrame("mem", |
||||
data.NewField("time", nil, timesMem), |
||||
data.NewField("mem", data.Labels{"host": "b", "env": "prod"}, []float64{3.0, 4.0}), |
||||
), |
||||
} |
||||
for _, f := range input { |
||||
f.Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, []time.Time{ |
||||
time.Unix(0, 0), // cpu a
|
||||
time.Unix(5, 0), // cpu no label
|
||||
time.Unix(10, 0), // cpu a
|
||||
time.Unix(10, 0), // mem
|
||||
time.Unix(15, 0), // cpu no label
|
||||
time.Unix(30, 0), // mem
|
||||
}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{ |
||||
fp(1.0), fp(9.0), fp(2.0), fp(3.0), fp(10.0), fp(4.0), |
||||
}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{ |
||||
"cpu", "cpu", "cpu", "mem", "cpu", "mem", |
||||
}), |
||||
data.NewField("env", nil, []*string{ |
||||
nil, nil, nil, sp("prod"), nil, sp("prod"), |
||||
}), |
||||
data.NewField("host", nil, []*string{ |
||||
sp("a"), nil, sp("a"), sp("b"), nil, sp("b"), |
||||
}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s", diff) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func TestConvertTimeSeriesMultiToFullLongWithDisplayName(t *testing.T) { |
||||
t.Run("SingleSeriesWithDisplayName", func(t *testing.T) { |
||||
times := []time.Time{time.Unix(0, 0), time.Unix(10, 0)} |
||||
|
||||
input := data.Frames{ |
||||
data.NewFrame("cpu", |
||||
data.NewField("time", nil, times), |
||||
func() *data.Field { |
||||
f := data.NewField("cpu", nil, []float64{1.0, 2.0}) |
||||
f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU Display"} |
||||
return f |
||||
}(), |
||||
), |
||||
} |
||||
input[0].Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, times), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{fp(1.0), fp(2.0)}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu"}), |
||||
data.NewField(SQLDisplayFieldName, nil, []*string{sp("CPU Display"), sp("CPU Display")}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) |
||||
} |
||||
}) |
||||
|
||||
t.Run("TwoSeriesMixedDisplayNames", func(t *testing.T) { |
||||
times := []time.Time{time.Unix(0, 0), time.Unix(10, 0)} |
||||
|
||||
input := data.Frames{ |
||||
data.NewFrame("cpu", |
||||
data.NewField("time", nil, times), |
||||
func() *data.Field { |
||||
f := data.NewField("cpu", data.Labels{"host": "a"}, []float64{1.0, 2.0}) |
||||
f.Config = &data.FieldConfig{DisplayNameFromDS: "CPU A"} |
||||
return f |
||||
}(), |
||||
), |
||||
data.NewFrame("cpu", |
||||
data.NewField("time", nil, times), |
||||
data.NewField("cpu", data.Labels{"host": "b"}, []float64{3.0, 4.0}), |
||||
), |
||||
} |
||||
for _, f := range input { |
||||
f.Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti} |
||||
} |
||||
|
||||
expected := data.NewFrame("", |
||||
data.NewField("time", nil, []time.Time{ |
||||
times[0], times[0], times[1], times[1], |
||||
}), |
||||
data.NewField(SQLValueFieldName, nil, []*float64{ |
||||
fp(1.0), fp(3.0), fp(2.0), fp(4.0), |
||||
}), |
||||
data.NewField(SQLMetricFieldName, nil, []string{"cpu", "cpu", "cpu", "cpu"}), |
||||
data.NewField(SQLDisplayFieldName, nil, []*string{ |
||||
sp("CPU A"), nil, sp("CPU A"), nil, |
||||
}), |
||||
data.NewField("host", nil, []*string{ |
||||
sp("a"), sp("b"), sp("a"), sp("b"), |
||||
}), |
||||
) |
||||
expected.Meta = &data.FrameMeta{Type: timeseriesFullLongType} |
||||
|
||||
output, err := ConvertToFullLong(input) |
||||
require.NoError(t, err) |
||||
require.Len(t, output, 1) |
||||
if diff := cmp.Diff(expected, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Mismatch (-want +got):\n%s", diff) |
||||
} |
||||
}) |
||||
} |
@ -1,311 +0,0 @@ |
||||
package expr |
||||
|
||||
import ( |
||||
"fmt" |
||||
"sort" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
) |
||||
|
||||
func ConvertToLong(frames data.Frames) (data.Frames, error) { |
||||
if len(frames) == 0 { |
||||
// general empty case for now
|
||||
return frames, nil |
||||
} |
||||
// Four Conversion Possible Cases
|
||||
// 1. NumericMulti -> NumericLong
|
||||
// 2. NumericWide -> NumericLong
|
||||
// 3. TimeSeriesMulti -> TimeSeriesLong
|
||||
// 4. TimeSeriesWide -> TimeSeriesLong
|
||||
|
||||
// Detect if input type is declared
|
||||
// First Check Frame Meta Type
|
||||
|
||||
var inputType data.FrameType |
||||
if frames[0].Meta != nil && frames[0].Meta.Type != "" { |
||||
inputType = frames[0].Meta.Type |
||||
} |
||||
|
||||
// TODO: Add some guessing of Type if not declared
|
||||
if inputType == "" { |
||||
return frames, fmt.Errorf("no input dataframe type set") |
||||
} |
||||
|
||||
if !supportedToLongConversion(inputType) { |
||||
return frames, fmt.Errorf("unsupported input dataframe type %s for SQL expression", inputType) |
||||
} |
||||
|
||||
toLong := getToLongConversionFunc(inputType) |
||||
if toLong == nil { |
||||
return frames, fmt.Errorf("could not get conversion function for input type %s", inputType) |
||||
} |
||||
|
||||
return toLong(frames) |
||||
} |
||||
|
||||
func convertNumericMultiToNumericLong(frames data.Frames) (data.Frames, error) { |
||||
// Apart from metadata, NumericMulti is basically NumericWide, except one frame per thing
|
||||
// so we collapse into wide and call the wide conversion
|
||||
wide := convertNumericMultiToNumericWide(frames) |
||||
return convertNumericWideToNumericLong(wide) |
||||
} |
||||
|
||||
func convertNumericMultiToNumericWide(frames data.Frames) data.Frames { |
||||
newFrame := data.NewFrame("") |
||||
for _, frame := range frames { |
||||
for _, field := range frame.Fields { |
||||
if !field.Type().Numeric() { |
||||
continue |
||||
} |
||||
newField := data.NewFieldFromFieldType(field.Type(), field.Len()) |
||||
newField.Name = field.Name |
||||
newField.Labels = field.Labels.Copy() |
||||
if field.Len() == 1 { |
||||
newField.Set(0, field.CopyAt(0)) |
||||
} |
||||
newFrame.Fields = append(newFrame.Fields, newField) |
||||
} |
||||
} |
||||
return data.Frames{newFrame} |
||||
} |
||||
|
||||
func convertNumericWideToNumericLong(frames data.Frames) (data.Frames, error) { |
||||
// Wide should only be one frame
|
||||
if len(frames) != 1 { |
||||
return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames)) |
||||
} |
||||
inputFrame := frames[0] |
||||
|
||||
// The Frame should have no more than one row
|
||||
if inputFrame.Rows() > 1 { |
||||
return nil, fmt.Errorf("expected no more than one row in the frame, but got %d", inputFrame.Rows()) |
||||
} |
||||
|
||||
// Gather:
|
||||
// - unique numeric Field Names, and
|
||||
// - unique Label Keys (from Numeric Fields only)
|
||||
// each one maps to a field in the output long Frame.
|
||||
uniqueNames := make([]string, 0) |
||||
uniqueKeys := make([]string, 0) |
||||
|
||||
uniqueNamesMap := make(map[string]data.FieldType) |
||||
uniqueKeysMap := make(map[string]struct{}) |
||||
|
||||
prints := make(map[string]int) |
||||
|
||||
registerPrint := func(labels data.Labels) { |
||||
fp := labels.Fingerprint().String() |
||||
if _, ok := prints[fp]; !ok { |
||||
prints[fp] = len(prints) |
||||
} |
||||
} |
||||
|
||||
for _, field := range inputFrame.Fields { |
||||
if field.Type().Numeric() { |
||||
if _, ok := uniqueNamesMap[field.Name]; !ok { |
||||
uniqueNames = append(uniqueNames, field.Name) |
||||
uniqueNamesMap[field.Name] = field.Type() |
||||
} |
||||
|
||||
if field.Labels != nil { |
||||
registerPrint(field.Labels) |
||||
for key := range field.Labels { |
||||
if _, ok := uniqueKeysMap[key]; !ok { |
||||
uniqueKeys = append(uniqueKeys, key) |
||||
} |
||||
uniqueKeysMap[key] = struct{}{} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Create new fields for output Long frame
|
||||
fields := make([]*data.Field, 0, len(uniqueNames)+len(uniqueKeys)) |
||||
|
||||
// Create the Numeric Fields, tracking the index of each field by name
|
||||
// Note: May want to use FloatAt and and prepopulate with NaN so missing
|
||||
// combinations of value can be NA instead of the zero value of 0.
|
||||
var nameIndexMap = make(map[string]int, len(uniqueNames)) |
||||
for i, name := range uniqueNames { |
||||
field := data.NewFieldFromFieldType(uniqueNamesMap[name], len(prints)) |
||||
field.Name = name |
||||
fields = append(fields, field) |
||||
nameIndexMap[name] = i |
||||
} |
||||
|
||||
// Create the String fields, tracking the index of each field by key
|
||||
var keyIndexMap = make(map[string]int, len(uniqueKeys)) |
||||
for i, k := range uniqueKeys { |
||||
fields = append(fields, data.NewField(k, nil, make([]string, len(prints)))) |
||||
keyIndexMap[k] = len(nameIndexMap) + i |
||||
} |
||||
|
||||
longFrame := data.NewFrame("", fields...) |
||||
|
||||
if inputFrame.Rows() == 0 { |
||||
return data.Frames{longFrame}, nil |
||||
} |
||||
|
||||
// Add Rows to the fields
|
||||
for _, field := range inputFrame.Fields { |
||||
if !field.Type().Numeric() { |
||||
continue |
||||
} |
||||
fieldIdx := prints[field.Labels.Fingerprint().String()] |
||||
longFrame.Fields[nameIndexMap[field.Name]].Set(fieldIdx, field.CopyAt(0)) |
||||
for key, value := range field.Labels { |
||||
longFrame.Fields[keyIndexMap[key]].Set(fieldIdx, value) |
||||
} |
||||
} |
||||
|
||||
return data.Frames{longFrame}, nil |
||||
} |
||||
|
||||
func convertTimeSeriesMultiToTimeSeriesLong(frames data.Frames) (data.Frames, error) { |
||||
// Collect all time values and ensure no duplicates
|
||||
timeSet := make(map[time.Time]struct{}) |
||||
labelKeys := make(map[string]struct{}) // Collect all unique label keys
|
||||
numericFields := make(map[string]struct{}) // Collect unique numeric field names
|
||||
|
||||
for _, frame := range frames { |
||||
for _, field := range frame.Fields { |
||||
if field.Type() == data.FieldTypeTime { |
||||
for i := 0; i < field.Len(); i++ { |
||||
t := field.At(i).(time.Time) |
||||
timeSet[t] = struct{}{} |
||||
} |
||||
} else if field.Type().Numeric() { |
||||
numericFields[field.Name] = struct{}{} |
||||
if field.Labels != nil { |
||||
for key := range field.Labels { |
||||
labelKeys[key] = struct{}{} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Create a sorted slice of unique time values
|
||||
times := make([]time.Time, 0, len(timeSet)) |
||||
for t := range timeSet { |
||||
times = append(times, t) |
||||
} |
||||
sort.Slice(times, func(i, j int) bool { return times[i].Before(times[j]) }) |
||||
|
||||
// Create output fields: Time, one numeric field per unique numeric name, and label fields
|
||||
timeField := data.NewField("Time", nil, times) |
||||
outputNumericFields := make(map[string]*data.Field) |
||||
for name := range numericFields { |
||||
outputNumericFields[name] = data.NewField(name, nil, make([]float64, len(times))) |
||||
} |
||||
outputLabelFields := make(map[string]*data.Field) |
||||
for key := range labelKeys { |
||||
outputLabelFields[key] = data.NewField(key, nil, make([]string, len(times))) |
||||
} |
||||
|
||||
// Map time to index for quick lookup
|
||||
timeIndexMap := make(map[time.Time]int, len(times)) |
||||
for i, t := range times { |
||||
timeIndexMap[t] = i |
||||
} |
||||
|
||||
// Populate output fields
|
||||
for _, frame := range frames { |
||||
var timeField *data.Field |
||||
for _, field := range frame.Fields { |
||||
if field.Type() == data.FieldTypeTime { |
||||
timeField = field |
||||
break |
||||
} |
||||
} |
||||
|
||||
if timeField == nil { |
||||
return nil, fmt.Errorf("no time field found in frame") |
||||
} |
||||
|
||||
for _, field := range frame.Fields { |
||||
if field.Type().Numeric() { |
||||
for i := 0; i < field.Len(); i++ { |
||||
t := timeField.At(i).(time.Time) |
||||
val, err := field.FloatAt(i) |
||||
if err != nil { |
||||
val = 0 // Default value for missing data
|
||||
} |
||||
idx := timeIndexMap[t] |
||||
if outputField, exists := outputNumericFields[field.Name]; exists { |
||||
outputField.Set(idx, val) |
||||
} |
||||
|
||||
// Add labels for the numeric field
|
||||
for key, value := range field.Labels { |
||||
if outputField, exists := outputLabelFields[key]; exists { |
||||
outputField.Set(idx, value) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Build the output frame
|
||||
outputFields := []*data.Field{timeField} |
||||
for _, field := range outputNumericFields { |
||||
outputFields = append(outputFields, field) |
||||
} |
||||
for _, field := range outputLabelFields { |
||||
outputFields = append(outputFields, field) |
||||
} |
||||
outputFrame := data.NewFrame("time_series_long", outputFields...) |
||||
|
||||
// Set metadata
|
||||
if outputFrame.Meta == nil { |
||||
outputFrame.Meta = &data.FrameMeta{} |
||||
} |
||||
outputFrame.Meta.Type = data.FrameTypeTimeSeriesLong |
||||
|
||||
return data.Frames{outputFrame}, nil |
||||
} |
||||
|
||||
func convertTimeSeriesWideToTimeSeriesLong(frames data.Frames) (data.Frames, error) { |
||||
// Wide should only be one frame
|
||||
if len(frames) != 1 { |
||||
return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames)) |
||||
} |
||||
inputFrame := frames[0] |
||||
longFrame, err := data.WideToLong(inputFrame) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to convert wide time series to long timeseries for sql expression: %w", err) |
||||
} |
||||
return data.Frames{longFrame}, nil |
||||
} |
||||
|
||||
func getToLongConversionFunc(inputType data.FrameType) func(data.Frames) (data.Frames, error) { |
||||
switch inputType { |
||||
case data.FrameTypeNumericMulti: |
||||
return convertNumericMultiToNumericLong |
||||
case data.FrameTypeNumericWide: |
||||
return convertNumericWideToNumericLong |
||||
case data.FrameTypeTimeSeriesMulti: |
||||
return convertTimeSeriesMultiToTimeSeriesLong |
||||
case data.FrameTypeTimeSeriesWide: |
||||
return convertTimeSeriesWideToTimeSeriesLong |
||||
default: |
||||
return convertErr |
||||
} |
||||
} |
||||
|
||||
func convertErr(_ data.Frames) (data.Frames, error) { |
||||
return nil, fmt.Errorf("unsupported input type for SQL expression") |
||||
} |
||||
|
||||
func supportedToLongConversion(inputType data.FrameType) bool { |
||||
switch inputType { |
||||
case data.FrameTypeNumericMulti, data.FrameTypeNumericWide: |
||||
return true |
||||
case data.FrameTypeTimeSeriesMulti, data.FrameTypeTimeSeriesWide: |
||||
return true |
||||
default: |
||||
return false |
||||
} |
||||
} |
@ -1,48 +0,0 @@ |
||||
package expr |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/google/go-cmp/cmp" |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestConvertNumericMultiToLong(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("test", |
||||
data.NewField("Value", data.Labels{"city": "MIA"}, []int64{5})), |
||||
data.NewFrame("test", |
||||
data.NewField("Value", data.Labels{"city": "LGA"}, []int64{7}), |
||||
), |
||||
} |
||||
expectedFrame := data.NewFrame("", |
||||
data.NewField("Value", nil, []int64{5, 7}), |
||||
data.NewField("city", nil, []string{"MIA", "LGA"}), |
||||
) |
||||
output, err := convertNumericMultiToNumericLong(input) |
||||
require.NoError(t, err) |
||||
|
||||
if diff := cmp.Diff(expectedFrame, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff) |
||||
} |
||||
} |
||||
|
||||
func TestConvertNumericWideToLong(t *testing.T) { |
||||
input := data.Frames{ |
||||
data.NewFrame("test", |
||||
data.NewField("Value", data.Labels{"city": "MIA"}, []int64{5}), |
||||
data.NewField("Value", data.Labels{"city": "LGA"}, []int64{7}), |
||||
), |
||||
} |
||||
expectedFrame := data.NewFrame("", |
||||
data.NewField("Value", nil, []int64{5, 7}), |
||||
data.NewField("city", nil, []string{"MIA", "LGA"}), |
||||
) |
||||
output, err := convertNumericWideToNumericLong(input) |
||||
require.NoError(t, err) |
||||
|
||||
if diff := cmp.Diff(expectedFrame, output[0], data.FrameTestCompareOptions()...); diff != "" { |
||||
require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff) |
||||
} |
||||
} |
Loading…
Reference in new issue