SQL Expressions: Always convert on type first (#106083)

fixes #103124
This commit is contained in:
Kyle Brandt 2025-05-28 12:00:37 -04:00 committed by GitHub
parent 64f321e430
commit 7f1a286ffb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 139 additions and 31 deletions

View File

@ -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)

View File

@ -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) {

View File

@ -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)
}
}
})
}
}