mirror of https://github.com/grafana/grafana.git
				
				
				
			
							parent
							
								
									64f321e430
								
							
						
					
					
						commit
						7f1a286ffb
					
				| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	// Convert it if Wide (has labels):
 | 
			
		||||
	if len(dataFrames) == 1 {
 | 
			
		||||
		for _, field := range dataFrames[0].Fields {
 | 
			
		||||
			if len(field.Labels) > 0 {
 | 
			
		||||
				needsConversion = true
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return result
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if needsConversion {
 | 
			
		||||
	var metaType data.FrameType
 | 
			
		||||
	if first.Meta != nil {
 | 
			
		||||
		metaType = first.Meta.Type
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if supportedToLongConversion(metaType) {
 | 
			
		||||
		convertedFrames, err := ConvertToFullLong(dataFrames)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return result, fmt.Errorf("failed to convert data frames to long format for sql: %w", err)
 | 
			
		||||
			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, nil
 | 
			
		||||
 | 
			
		||||
		return result
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Otherwise it is already Long format; return as is
 | 
			
		||||
	result.Values = mathexp.Values{
 | 
			
		||||
		mathexp.TableData{Frame: dataFrames[0]},
 | 
			
		||||
	// 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
 | 
			
		||||
	}
 | 
			
		||||
	return result, nil
 | 
			
		||||
	for _, frame := range dataFrames {
 | 
			
		||||
		for _, field := range frame.Fields {
 | 
			
		||||
			if len(field.Labels) > 0 {
 | 
			
		||||
				result.Error = fmt.Errorf("frame has labels but frame type is missing or unsupported for sql conversion")
 | 
			
		||||
				return result
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Can pass through as table without conversion
 | 
			
		||||
	result.Values = mathexp.Values{
 | 
			
		||||
		mathexp.TableData{Frame: first},
 | 
			
		||||
	}
 | 
			
		||||
	return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getResponseFrame(logger *log.ConcreteLogger, resp *backend.QueryDataResponse, refID string) (data.Frames, error) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue