diff --git a/pkg/expr/convert_to_full_long.go b/pkg/expr/convert_to_full_long.go index 94dc2648bd6..863e45b39df 100644 --- a/pkg/expr/convert_to_full_long.go +++ b/pkg/expr/convert_to_full_long.go @@ -30,10 +30,6 @@ func ConvertToFullLong(frames data.Frames) (data.Frames, error) { 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) diff --git a/pkg/expr/converter.go b/pkg/expr/converter.go index b328c5eca42..e4c444c5ce7 100644 --- a/pkg/expr/converter.go +++ b/pkg/expr/converter.go @@ -30,8 +30,8 @@ func (c *ResultConverter) Convert(ctx context.Context, } if forSqlInput { - results, err := handleSqlInput(frames) - return "sql input", results, err + results := handleSqlInput(frames) + return "sql input", results, nil } var dt data.FrameType @@ -126,41 +126,71 @@ func (c *ResultConverter) Convert(ctx context.Context, }, nil } -// copied from pkg/expr/nodes.go from within the Execute method -func handleSqlInput(dataFrames data.Frames) (mathexp.Results, error) { +// handleSqlInput normalizes input DataFrames into a single dataframe with no labels for use with SQL expressions. +// +// It handles three cases: +// 1. If the input declares a supported time series or numeric kind in the wide or multi format (via FrameMeta.Type), it converts to a full-long formatted table using ConvertToFullLong. +// 2. If the input is a single frame (no labels, no declared type), it passes through as-is. +// 3. If the input has multiple frames or label metadata but lacks a supported type, it returns an error. +func handleSqlInput(dataFrames data.Frames) mathexp.Results { var result mathexp.Results - var needsConversion bool - // Convert it if Multi: - if len(dataFrames) > 1 { - needsConversion = true + + // dataframes len > 0 is checked in the caller -- Convert + first := dataFrames[0] + + // Single Frame no data case + // Note: In the case of a support Frame Type, we may want to return the matching schema + // with no rows (e.g. include the `__value__` column). But not sure about this at this time. + if len(dataFrames) == 1 && len(first.Fields) == 0 { + result.Values = mathexp.Values{ + mathexp.TableData{Frame: first}, + } + + return result } - // Convert it if Wide (has labels): - if len(dataFrames) == 1 { - for _, field := range dataFrames[0].Fields { + var metaType data.FrameType + if first.Meta != nil { + metaType = first.Meta.Type + } + + if supportedToLongConversion(metaType) { + convertedFrames, err := ConvertToFullLong(dataFrames) + if err != nil { + result.Error = fmt.Errorf("failed to convert data frames to long format for SQL: %w", err) + } + + if len(convertedFrames) == 0 { + result.Error = fmt.Errorf("conversion succeeded but returned no frames") + return result + } + + result.Values = mathexp.Values{ + mathexp.TableData{Frame: convertedFrames[0]}, + } + + return result + } + + // If Meta.Type is not supported, but there are labels or more than 1 frame, fail fast + if len(dataFrames) > 1 { + result.Error = fmt.Errorf("response has more than one frame but frame type is missing or unsupported for sql conversion") + return result + } + for _, frame := range dataFrames { + for _, field := range frame.Fields { if len(field.Labels) > 0 { - needsConversion = true - break + result.Error = fmt.Errorf("frame has labels but frame type is missing or unsupported for sql conversion") + return result } } } - if needsConversion { - convertedFrames, err := ConvertToFullLong(dataFrames) - if err != nil { - return result, fmt.Errorf("failed to convert data frames to long format for sql: %w", err) - } - result.Values = mathexp.Values{ - mathexp.TableData{Frame: convertedFrames[0]}, - } - return result, nil - } - - // Otherwise it is already Long format; return as is + // Can pass through as table without conversion result.Values = mathexp.Values{ - mathexp.TableData{Frame: dataFrames[0]}, + mathexp.TableData{Frame: first}, } - return result, nil + return result } func getResponseFrame(logger *log.ConcreteLogger, resp *backend.QueryDataResponse, refID string) (data.Frames, error) { diff --git a/pkg/expr/converter_test.go b/pkg/expr/converter_test.go index 45d04169c32..15c45ebf0e2 100644 --- a/pkg/expr/converter_test.go +++ b/pkg/expr/converter_test.go @@ -120,3 +120,85 @@ func TestConvertDataFramesToResults(t *testing.T) { }) }) } + +func TestHandleSqlInput(t *testing.T) { + tests := []struct { + name string + frames data.Frames + expectErr string + expectFrame bool + }{ + { + name: "single frame with no fields and no type is passed through", + frames: data.Frames{data.NewFrame("")}, + expectFrame: true, + }, + { + name: "single frame with no fields but type timeseries-multi is passed through", + frames: data.Frames{data.NewFrame("").SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti})}, + expectFrame: true, + }, + { + name: "single frame, no labels, no type → passes through", + frames: data.Frames{ + data.NewFrame("", + data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), + data.NewField("value", nil, []*float64{fp(2)}), + ), + }, + expectFrame: true, + }, + { + name: "single frame with labels, but missing FrameMeta.Type → error", + frames: data.Frames{ + data.NewFrame("", + data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), + data.NewField("value", data.Labels{"foo": "bar"}, []*float64{fp(2)}), + ), + }, + expectErr: "frame has labels but frame type is missing or unsupported", + }, + { + name: "multiple frames, no type → error", + frames: data.Frames{ + data.NewFrame("", + data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), + data.NewField("value", nil, []*float64{fp(2)}), + ), + data.NewFrame("", + data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), + data.NewField("value", nil, []*float64{fp(2)}), + ), + }, + expectErr: "response has more than one frame but frame type is missing or unsupported", + }, + { + name: "supported type (timeseries-multi) triggers ConvertToFullLong", + frames: data.Frames{ + data.NewFrame("", + data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), + data.NewField("value", data.Labels{"host": "a"}, []*float64{fp(2)}), + ).SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti}), + }, + expectFrame: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + res := handleSqlInput(tc.frames) + + if tc.expectErr != "" { + require.Error(t, res.Error) + require.ErrorContains(t, res.Error, tc.expectErr) + } else { + require.NoError(t, res.Error) + if tc.expectFrame { + require.Len(t, res.Values, 1) + require.IsType(t, mathexp.TableData{}, res.Values[0]) + assert.NotNil(t, res.Values[0].(mathexp.TableData).Frame) + } + } + }) + } +}