| 
									
										
										
										
											2024-02-28 05:16:00 +08:00
										 |  |  | package expr | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2025-03-12 01:14:33 +08:00
										 |  |  | 	"context" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"net/http" | 
					
						
							| 
									
										
										
										
											2024-02-28 05:16:00 +08:00
										 |  |  | 	"strings" | 
					
						
							|  |  |  | 	"testing" | 
					
						
							| 
									
										
										
										
											2025-03-12 01:14:33 +08:00
										 |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 23:13:42 +08:00
										 |  |  | 	"github.com/grafana/grafana-plugin-sdk-go/backend/log" | 
					
						
							| 
									
										
										
										
											2025-03-12 01:14:33 +08:00
										 |  |  | 	"github.com/grafana/grafana-plugin-sdk-go/data" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/expr/mathexp" | 
					
						
							| 
									
										
										
										
											2025-04-11 02:51:44 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/expr/metrics" | 
					
						
							|  |  |  | 	"github.com/prometheus/client_golang/prometheus/testutil" | 
					
						
							| 
									
										
										
										
											2025-03-12 01:14:33 +08:00
										 |  |  | 	"github.com/stretchr/testify/require" | 
					
						
							| 
									
										
										
										
											2025-08-25 23:13:42 +08:00
										 |  |  | 	"go.opentelemetry.io/otel/attribute" | 
					
						
							| 
									
										
										
										
											2025-08-07 20:43:39 +08:00
										 |  |  | 	"go.opentelemetry.io/otel/codes" | 
					
						
							| 
									
										
										
										
											2025-03-12 01:14:33 +08:00
										 |  |  | 	"go.opentelemetry.io/otel/trace" | 
					
						
							| 
									
										
										
										
											2024-02-28 05:16:00 +08:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func TestNewCommand(t *testing.T) { | 
					
						
							| 
									
										
										
										
											2025-08-25 23:13:42 +08:00
										 |  |  | 	cmd, err := NewSQLCommand(t.Context(), log.NewNullLogger(), "a", "", "select a from foo, bar", 0, 0, 0) | 
					
						
							| 
									
										
										
										
											2024-02-28 05:16:00 +08:00
										 |  |  | 	if err != nil && strings.Contains(err.Error(), "feature is not enabled") { | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		t.Fail() | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, v := range cmd.varsToQuery { | 
					
						
							|  |  |  | 		if strings.Contains("foo bar", v) { | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		t.Fail() | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-03-12 01:14:33 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | // Helper function for creating test data
 | 
					
						
							|  |  |  | func createFrameWithRowsAndCols(rows int, cols int) *data.Frame { | 
					
						
							|  |  |  | 	frame := data.NewFrame("dummy") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for c := 0; c < cols; c++ { | 
					
						
							|  |  |  | 		values := make([]string, rows) | 
					
						
							|  |  |  | 		frame.Fields = append(frame.Fields, data.NewField(fmt.Sprintf("col%d", c), nil, values)) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return frame | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func TestSQLCommandCellLimits(t *testing.T) { | 
					
						
							|  |  |  | 	tests := []struct { | 
					
						
							|  |  |  | 		name          string | 
					
						
							|  |  |  | 		limit         int64 | 
					
						
							|  |  |  | 		frames        []*data.Frame | 
					
						
							|  |  |  | 		vars          []string | 
					
						
							|  |  |  | 		expectError   bool | 
					
						
							|  |  |  | 		errorContains string | 
					
						
							|  |  |  | 	}{ | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			name:  "single (long) frame within cell limit", | 
					
						
							|  |  |  | 			limit: 10, | 
					
						
							|  |  |  | 			frames: []*data.Frame{ | 
					
						
							|  |  |  | 				createFrameWithRowsAndCols(10, 1), // 10 cells
 | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			vars: []string{"foo"}, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			name:  "single (wide) frame within cell limit", | 
					
						
							|  |  |  | 			limit: 10, | 
					
						
							|  |  |  | 			frames: []*data.Frame{ | 
					
						
							|  |  |  | 				createFrameWithRowsAndCols(1, 10), // 10 cells
 | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			vars: []string{"foo"}, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			name:  "multiple frames within cell limit", | 
					
						
							|  |  |  | 			limit: 12, | 
					
						
							|  |  |  | 			frames: []*data.Frame{ | 
					
						
							|  |  |  | 				createFrameWithRowsAndCols(2, 3), // 6 cells
 | 
					
						
							|  |  |  | 				createFrameWithRowsAndCols(2, 3), // 6 cells
 | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			vars: []string{"foo", "bar"}, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			name:  "single (long) frame exceeds cell limit", | 
					
						
							|  |  |  | 			limit: 9, | 
					
						
							|  |  |  | 			frames: []*data.Frame{ | 
					
						
							|  |  |  | 				createFrameWithRowsAndCols(10, 1), // 10 cells > 9 limit
 | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			vars:          []string{"foo"}, | 
					
						
							|  |  |  | 			expectError:   true, | 
					
						
							| 
									
										
										
										
											2025-08-25 23:13:42 +08:00
										 |  |  | 			errorContains: "exceeded the configured limit", | 
					
						
							| 
									
										
										
										
											2025-03-12 01:14:33 +08:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			name:  "single (wide) frame exceeds cell limit", | 
					
						
							|  |  |  | 			limit: 9, | 
					
						
							|  |  |  | 			frames: []*data.Frame{ | 
					
						
							|  |  |  | 				createFrameWithRowsAndCols(1, 10), // 10 cells > 9 limit
 | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			vars:          []string{"foo"}, | 
					
						
							|  |  |  | 			expectError:   true, | 
					
						
							| 
									
										
										
										
											2025-08-25 23:13:42 +08:00
										 |  |  | 			errorContains: "exceeded the configured limit", | 
					
						
							| 
									
										
										
										
											2025-03-12 01:14:33 +08:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			name:  "multiple frames exceed cell limit", | 
					
						
							|  |  |  | 			limit: 11, | 
					
						
							|  |  |  | 			frames: []*data.Frame{ | 
					
						
							|  |  |  | 				createFrameWithRowsAndCols(2, 3), // 6 cells
 | 
					
						
							|  |  |  | 				createFrameWithRowsAndCols(2, 3), // 6 cells
 | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			vars:          []string{"foo", "bar"}, | 
					
						
							|  |  |  | 			expectError:   true, | 
					
						
							| 
									
										
										
										
											2025-08-25 23:13:42 +08:00
										 |  |  | 			errorContains: "exceeded the configured limit", | 
					
						
							| 
									
										
										
										
											2025-03-12 01:14:33 +08:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			name:  "limit of 0 means no limit: allow large frame", | 
					
						
							|  |  |  | 			limit: 0, | 
					
						
							|  |  |  | 			frames: []*data.Frame{ | 
					
						
							|  |  |  | 				createFrameWithRowsAndCols(200000, 1), // 200,000 cells
 | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			vars: []string{"foo", "bar"}, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, tt := range tests { | 
					
						
							|  |  |  | 		t.Run(tt.name, func(t *testing.T) { | 
					
						
							| 
									
										
										
										
											2025-08-25 23:13:42 +08:00
										 |  |  | 			cmd, err := NewSQLCommand(t.Context(), log.New(), "a", "", "select a from foo, bar", tt.limit, 0, 0) | 
					
						
							| 
									
										
										
										
											2025-03-12 01:14:33 +08:00
										 |  |  | 			require.NoError(t, err, "Failed to create SQL command") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			vars := mathexp.Vars{} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			for i, frame := range tt.frames { | 
					
						
							|  |  |  | 				vars[tt.vars[i]] = mathexp.Results{ | 
					
						
							|  |  |  | 					Values: mathexp.Values{mathexp.TableData{Frame: frame}}, | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-17 21:23:56 +08:00
										 |  |  | 			res, _ := cmd.Execute(context.Background(), time.Now(), vars, &testTracer{}, metrics.NewTestMetrics()) | 
					
						
							| 
									
										
										
										
											2025-03-12 01:14:33 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			if tt.expectError { | 
					
						
							| 
									
										
										
										
											2025-06-17 21:23:56 +08:00
										 |  |  | 				require.Error(t, res.Error) | 
					
						
							|  |  |  | 				require.ErrorContains(t, res.Error, tt.errorContains) | 
					
						
							| 
									
										
										
										
											2025-03-12 01:14:33 +08:00
										 |  |  | 			} else { | 
					
						
							|  |  |  | 				require.NoError(t, err) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-11 02:51:44 +08:00
										 |  |  | func TestSQLCommandMetrics(t *testing.T) { | 
					
						
							|  |  |  | 	// Create test metrics
 | 
					
						
							|  |  |  | 	m := metrics.NewTestMetrics() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Create a command
 | 
					
						
							| 
									
										
										
										
											2025-08-25 23:13:42 +08:00
										 |  |  | 	cmd, err := NewSQLCommand(t.Context(), log.NewNullLogger(), "A", "someformat", "select * from foo", 0, 0, 0) | 
					
						
							| 
									
										
										
										
											2025-04-11 02:51:44 +08:00
										 |  |  | 	require.NoError(t, err) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Execute successful command
 | 
					
						
							|  |  |  | 	_, err = cmd.Execute(context.Background(), time.Now(), mathexp.Vars{}, &testTracer{}, m) | 
					
						
							|  |  |  | 	require.NoError(t, err) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 23:13:42 +08:00
										 |  |  | 	// Verify count metric was recorded
 | 
					
						
							|  |  |  | 	require.Equal(t, 1, testutil.CollectAndCount(m.SqlCommandCount), "Expected count metric to be recorded") | 
					
						
							| 
									
										
										
										
											2025-04-11 02:51:44 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Verify duration was recorded
 | 
					
						
							|  |  |  | 	require.Equal(t, 1, testutil.CollectAndCount(m.SqlCommandDuration), "Expected duration metric to be recorded") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Verify cell count was recorded
 | 
					
						
							|  |  |  | 	require.Equal(t, 1, testutil.CollectAndCount(m.SqlCommandCellCount), "Expected cell count metric to be recorded") | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-25 23:13:42 +08:00
										 |  |  | func TestHandleSqlInput(t *testing.T) { | 
					
						
							|  |  |  | 	tests := []struct { | 
					
						
							|  |  |  | 		name        string | 
					
						
							|  |  |  | 		frames      data.Frames | 
					
						
							|  |  |  | 		expectErr   string | 
					
						
							|  |  |  | 		expectFrame bool | 
					
						
							|  |  |  | 		converted   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: "labels in the response that can not be mapped to a table", | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			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: "more than one dataframe that can not be automatically mapped to a single table", | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			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, | 
					
						
							|  |  |  | 			converted:   true, | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2025-09-03 02:49:04 +08:00
										 |  |  | 		{ | 
					
						
							|  |  |  | 			name: "supported type (timeseries-multi) but malformed returns error", | 
					
						
							|  |  |  | 			frames: data.Frames{ | 
					
						
							|  |  |  | 				data.NewFrame("", | 
					
						
							|  |  |  | 					data.NewField("time", nil, []string{"1"}), // string is not valid for time field
 | 
					
						
							|  |  |  | 					data.NewField("value", data.Labels{"host": "a"}, []*float64{fp(2)}), | 
					
						
							|  |  |  | 				).SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti}), | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			expectErr: "missing time field", | 
					
						
							|  |  |  | 			converted: true, | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2025-08-25 23:13:42 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, tc := range tests { | 
					
						
							|  |  |  | 		t.Run(tc.name, func(t *testing.T) { | 
					
						
							|  |  |  | 			res, c := handleSqlInput(t.Context(), &testTracer{}, "a", map[string]struct{}{"b": {}}, "fakeDS", tc.frames) | 
					
						
							|  |  |  | 			require.Equal(t, tc.converted, c, "conversion bool mismatch") | 
					
						
							|  |  |  | 			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]) | 
					
						
							|  |  |  | 					require.NotNil(t, res.Values[0].(mathexp.TableData).Frame) | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-12 01:14:33 +08:00
										 |  |  | type testTracer struct { | 
					
						
							|  |  |  | 	trace.Tracer | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (t *testTracer) Start(ctx context.Context, name string, s ...trace.SpanStartOption) (context.Context, trace.Span) { | 
					
						
							|  |  |  | 	return ctx, &testSpan{} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | func (t *testTracer) Inject(context.Context, http.Header, trace.Span) { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type testSpan struct { | 
					
						
							|  |  |  | 	trace.Span | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (ts *testSpan) End(opt ...trace.SpanEndOption) { | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2025-08-07 20:43:39 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | func (ts *testSpan) RecordError(err error, opt ...trace.EventOption) { | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (ts *testSpan) SetStatus(code codes.Code, msg string) {} | 
					
						
							| 
									
										
										
										
											2025-08-25 23:13:42 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | func (ts *testSpan) AddEvent(name string, opts ...trace.EventOption) {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (ts *testSpan) SetAttributes(kv ...attribute.KeyValue) {} | 
					
						
							| 
									
										
										
										
											2025-09-04 01:25:44 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | func (ts *testSpan) SpanContext() trace.SpanContext { | 
					
						
							|  |  |  | 	return trace.SpanContext{} | 
					
						
							|  |  |  | } |