Repository: grafana/grafana-plugin-sdk-go (go)

Issue:
Add field to data queries to ease "routing"/looking up query type
Core data datasources in Grafana are supporting/handling multiple query types and/or data source types.
Azure monitor example:
https://github.com/grafana/grafana/blob/c6cc840ceb6ff64dc45200888477b14c89bc0785/pkg/tsdb/azuremonitor/azuremonitor.go#L49-L65
CloudWatch example:
https://github.com/grafana/grafana/blob/c6cc840ceb6ff64dc45200888477b14c89bc0785/pkg/tsdb/cloudwatch/cloudwatch.go#L47-L63

We would like to enhance the data query request to include a new field that can be used to identify "query type". SDK can then provide some basic routing/map of "query type" to query handler and by that supporting several query handlers without having to parse the model json first to understand the query type.

With that plugin developer should be able to register a handler for a "query type" and marschal model json to struct for type safe handling.

Pull request description:
Query type mux
Remembered we haven't done anything in the SDK regarding to [queryType](https://github.com/grafana/grafana/blob/21cbcde15fcb6ed7182e3398063ced4f8a379d71/packages/grafana-data/src/types/datasource.ts#L397).

Closes #75

Patch:
diff --git a/backend/convert_from_protobuf.go b/backend/convert_from_protobuf.go
index 4b46365..368c962 100644
--- a/backend/convert_from_protobuf.go
+++ b/backend/convert_from_protobuf.go
@@ -91,6 +91,7 @@ func (f ConvertFromProtobuf) TimeRange(proto *pluginv2.TimeRange) TimeRange {
 func (f ConvertFromProtobuf) DataQuery(proto *pluginv2.DataQuery) *DataQuery {
 	return &DataQuery{
 		RefID:         proto.RefId,
+		QueryType:     proto.QueryType,
 		MaxDataPoints: proto.MaxDataPoints,
 		TimeRange:     f.TimeRange(proto.TimeRange),
 		Interval:      time.Duration(proto.IntervalMS) * time.Millisecond,
diff --git a/backend/convert_to_protobuf.go b/backend/convert_to_protobuf.go
index 86b1093..a4eed38 100644
--- a/backend/convert_to_protobuf.go
+++ b/backend/convert_to_protobuf.go
@@ -111,6 +111,7 @@ func (t ConvertToProtobuf) CheckHealthResponse(res *CheckHealthResult) *pluginv2
 func (t ConvertToProtobuf) DataQuery(q DataQuery) *pluginv2.DataQuery {
 	return &pluginv2.DataQuery{
 		RefId:         q.RefID,
+		QueryType:     q.QueryType,
 		MaxDataPoints: q.MaxDataPoints,
 		IntervalMS:    q.Interval.Milliseconds(),
 		TimeRange:     t.TimeRange(q.TimeRange),
@@ -132,11 +133,18 @@ func (t ConvertToProtobuf) QueryDataRequest(req *QueryDataRequest) *pluginv2.Que
 }
 
 // QueryDataResponse converts the SDK version of a QueryDataResponse to the protobuf version.
+// It will set the RefID on the frames to the RefID key in Responses if a Frame's
+// RefId property is an empty string.
 func (t ConvertToProtobuf) QueryDataResponse(res *QueryDataResponse) (*pluginv2.QueryDataResponse, error) {
 	pQDR := &pluginv2.QueryDataResponse{
 		Responses: make(map[string]*pluginv2.DataResponse, len(res.Responses)),
 	}
 	for refID, dr := range res.Responses {
+		for _, f := range dr.Frames {
+			if f.RefID == "" {
+				f.RefID = refID
+			}
+		}
 		encodedFrames, err := dr.Frames.MarshalArrow()
 		if err != nil {
 			return nil, err
diff --git a/backend/data.go b/backend/data.go
index b4ab6f1..6fa6293 100644
--- a/backend/data.go
+++ b/backend/data.go
@@ -14,6 +14,10 @@ type QueryDataHandler interface {
 	// req contains the queries []DataQuery (where each query contains RefID as a unique identifer).
 	// The QueryDataResponse contains a map of RefID to the response for each query, and each response
 	// contains Frames ([]*Frame).
+	//
+	// The Frames' RefID property, when it is an empty string, will be automatically set to
+	// the RefID in QueryDataResponse.Responses map. This is done before the QueryDataResponse is
+	// sent to Grafana. Therefore one does not need to be set that property on frames when using this method.
 	QueryData(ctx context.Context, req *QueryDataRequest) (*QueryDataResponse, error)
 }
 
@@ -31,6 +35,10 @@ type DataQuery struct {
 	// RefID is the unique identifer of the query, set by the frontend call.
 	RefID string
 
+	// QueryType is an optional identifier for the type of query.
+	// It can be used to distinguish different types of queries.
+	QueryType string
+
 	// MaxDataPoints is the maximum number of datapoints that should be returned from a time series query.
 	MaxDataPoints int64
 
@@ -59,6 +67,8 @@ func NewQueryDataResponse() *QueryDataResponse {
 }
 
 // Responses is a map of RefIDs (Unique Query ID) to DataResponses.
+// The QueryData method the QueryDataHandler method will set the RefId
+// property on the DataRespones' frames based on these RefIDs.
 type Responses map[string]DataResponse
 
 // DataResponse contains the results from a DataQuery.
diff --git a/backend/datasource/doc.go b/backend/datasource/doc.go
new file mode 100644
index 0000000..8a61b05
--- /dev/null
+++ b/backend/datasource/doc.go
@@ -0,0 +1,2 @@
+// Package datasource provides utilities for data source plugins.
+package datasource
diff --git a/backend/datasource/query_type_mux.go b/backend/datasource/query_type_mux.go
new file mode 100644
index 0000000..107e624
--- /dev/null
+++ b/backend/datasource/query_type_mux.go
@@ -0,0 +1,135 @@
+package datasource
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/grafana/grafana-plugin-sdk-go/backend/log"
+
+	"github.com/grafana/grafana-plugin-sdk-go/backend"
+)
+
+// QueryTypeHandlerFunc is an adapter to allow the use of
+// ordinary functions as backend.QueryDataHandler. If f is a function
+// with the appropriate signature, QueryTypeHandlerFunc(f) is a
+// Handler that calls f.
+type QueryTypeHandlerFunc func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error)
+
+// QueryData calls f(ctx, req).
+func (fn QueryTypeHandlerFunc) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
+	return fn(ctx, req)
+}
+
+// QueryTypeMux is a query type multiplexer.
+type QueryTypeMux struct {
+	m               map[string]backend.QueryDataHandler
+	fallbackHandler backend.QueryDataHandler
+}
+
+// NewQueryTypeMux allocates and returns a new QueryTypeMux.
+func NewQueryTypeMux() *QueryTypeMux {
+	return new(QueryTypeMux)
+}
+
+// Handle registers the handler for the given query type.
+//
+// Providing an empty queryType registers the handler as a fallback handler
+// that will be called when query type doesn't match any registered handlers.
+// If handler is nil, Handle panics.
+// If a handler already exists for queryType, Handle panics.
+func (mux *QueryTypeMux) Handle(queryType string, handler backend.QueryDataHandler) {
+	if handler == nil {
+		panic("datasource: nil handler")
+	}
+
+	if mux.m == nil {
+		mux.m = map[string]backend.QueryDataHandler{}
+		mux.fallbackHandler = QueryTypeHandlerFunc(fallbackHandler)
+	}
+
+	if _, exist := mux.m[queryType]; exist {
+		panic("datasource: multiple registrations for " + queryType)
+	}
+
+	if queryType == "" {
+		log.DefaultLogger.Debug("datasource: registering query type fallback handler")
+		mux.fallbackHandler = handler
+		return
+	}
+
+	log.DefaultLogger.Debug("datasource: registering query type handler", "queryType", queryType)
+	mux.m[queryType] = handler
+}
+
+// HandleFunc registers the handler function for the given query type.
+//
+// Providing an empty queryType registers the handler as a fallback handler
+// that will be called when query type doesn't match any registered handlers.
+// If handler is nil, Handle panics.
+// If a handler already exists for queryType, Handle panics.
+func (mux *QueryTypeMux) HandleFunc(queryType string, handler func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error)) {
+	mux.Handle(queryType, QueryTypeHandlerFunc(handler))
+}
+
+// QueryData dispatches the request to the handler(s) whose
+// query type matches the request queries query type.
+func (mux *QueryTypeMux) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
+	type requestHandler struct {
+		handler backend.QueryDataHandler
+		request *backend.QueryDataRequest
+	}
+
+	requests := map[string]requestHandler{}
+	for _, q := range req.Queries {
+		qt, handler := mux.getHandler(q.QueryType)
+		if _, exists := requests[qt]; !exists {
+			requests[qt] = requestHandler{
+				handler: handler,
+				request: &backend.QueryDataRequest{
+					PluginContext: req.PluginContext,
+					Headers:       req.Headers,
+					Queries:       []backend.DataQuery{},
+				},
+			}
+		}
+		requests[qt].request.Queries = append(requests[qt].request.Queries, q)
+	}
+
+	responses := backend.Responses{}
+	for _, rh := range requests {
+		qtResponse, err := rh.handler.QueryData(ctx, rh.request)
+		if err != nil {
+			return nil, err
+		}
+
+		for k, v := range qtResponse.Responses {
+			responses[k] = v
+		}
+	}
+
+	return &backend.QueryDataResponse{
+		Responses: responses,
+	}, nil
+}
+
+func (mux *QueryTypeMux) getHandler(queryType string) (string, backend.QueryDataHandler) {
+	handler, exists := mux.m[queryType]
+	if !exists {
+		return "", mux.fallbackHandler
+	}
+
+	return queryType, handler
+}
+
+func fallbackHandler(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
+	responses := backend.Responses{}
+	for _, q := range req.Queries {
+		responses[q.RefID] = backend.DataResponse{
+			Error: fmt.Errorf("no handler found for query type '%s'", q.QueryType),
+		}
+	}
+
+	return &backend.QueryDataResponse{
+		Responses: responses,
+	}, nil
+}
diff --git a/backend/doc.go b/backend/doc.go
new file mode 100644
index 0000000..6d44b45
--- /dev/null
+++ b/backend/doc.go
@@ -0,0 +1,2 @@
+// Package backend provides SDK handler interfaces and contracts for implementing and serving backend plugins.
+package backend
diff --git a/data/frame.go b/data/frame.go
index ca1431e..bc308ae 100644
--- a/data/frame.go
+++ b/data/frame.go
@@ -34,7 +34,7 @@ type Frame struct {
 	// All Fields must be of the same the length when marshalling the Frame for transmission.
 	Fields []*Field
 
-	// RefID is a property that can be set to match a Frame to its orginating query
+	// RefID is a property that can be set to match a Frame to its orginating query.
 	RefID string
 
 	// Meta is metadata about the Frame, and includes space for custom metadata.
diff --git a/data/frame_meta.go b/data/frame_meta.go
index 98f5134..83f7aaf 100644
--- a/data/frame_meta.go
+++ b/data/frame_meta.go
@@ -1,6 +1,9 @@
 package data
 
-import "encoding/json"
+import (
+	"encoding/json"
+	"fmt"
+)
 
 // FrameMeta matches:
 // https://github.com/grafana/grafana/blob/master/packages/grafana-data/src/types/data.ts#L11
@@ -80,6 +83,31 @@ func (n NoticeSeverity) String() string {
 	return ""
 }
 
+// MarshalJSON implements the json.Marshaler interface.
+func (n NoticeSeverity) MarshalJSON() ([]byte, error) {
+	return json.Marshal(n.String())
+}
+
+// UnmarshalJSON implements the json.Unmarshaler interface.
+func (n *NoticeSeverity) UnmarshalJSON(b []byte) error {
+	var s string
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	switch s {
+	case "info":
+		*n = NoticeSeverityInfo
+	case "warning":
+		*n = NoticeSeverityWarning
+	case "error":
+		*n = NoticeSeverityError
+	default:
+		return fmt.Errorf("unrecognized notice severity %v", s)
+	}
+	return nil
+}
+
 // InspectType is a type for the Inspect property of a Notice.
 type InspectType int
 
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..a45672d
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,5 @@
+/*
+Package sdk is the root of the packages used to access Grafana Plugin SDK for Go.
+See https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go for a full list of sub-packages.
+*/
+package sdk // import "github.com/grafana/grafana-plugin-sdk-go"
diff --git a/genproto/pluginv2/backend.pb.go b/genproto/pluginv2/backend.pb.go
index 06122ba..b11e40a 100644
--- a/genproto/pluginv2/backend.pb.go
+++ b/genproto/pluginv2/backend.pb.go
@@ -593,6 +593,7 @@ type DataQuery struct {
 	IntervalMS           int64      `protobuf:"varint,3,opt,name=intervalMS,proto3" json:"intervalMS,omitempty"`
 	TimeRange            *TimeRange `protobuf:"bytes,4,opt,name=timeRange,proto3" json:"timeRange,omitempty"`
 	Json                 []byte     `protobuf:"bytes,5,opt,name=json,proto3" json:"json,omitempty"`
+	QueryType            string     `protobuf:"bytes,6,opt,name=queryType,proto3" json:"queryType,omitempty"`
 	XXX_NoUnkeyedLiteral struct{}   `json:"-"`
 	XXX_unrecognized     []byte     `json:"-"`
 	XXX_sizecache        int32      `json:"-"`
@@ -658,6 +659,13 @@ func (m *DataQuery) GetJson() []byte {
 	return nil
 }
 
+func (m *DataQuery) GetQueryType() string {
+	if m != nil {
+		return m.QueryType
+	}
+	return ""
+}
+
 // QueryDataRequest
 type QueryDataRequest struct {
 	PluginContext *PluginContext `protobuf:"bytes,1,opt,name=pluginContext,proto3" json:"pluginContext,omitempty"`
@@ -1056,81 +1064,82 @@ func init() {
 }
 
 var fileDescriptor_5ab9ba5b8d8b2ba5 = []byte{
-	// 1176 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x57, 0xdb, 0x6e, 0x23, 0x45,
-	0x13, 0xfe, 0xc7, 0xc7, 0x4c, 0xd9, 0x89, 0xfc, 0xb7, 0xb3, 0x59, 0xcb, 0x24, 0x8b, 0x35, 0x8a,
-	0x84, 0x09, 0x60, 0x81, 0xb9, 0x41, 0x41, 0x5c, 0xe4, 0x24, 0xe5, 0xb0, 0x39, 0xd0, 0x4e, 0x00,
-	0x2d, 0xe2, 0xa2, 0x3d, 0xd3, 0xb1, 0x87, 0x8c, 0xa7, 0xbd, 0xdd, 0xed, 0x68, 0x23, 0x1e, 0x80,
-	0x67, 0x40, 0xe2, 0x19, 0xf6, 0x86, 0x7b, 0xee, 0x78, 0x06, 0x2e, 0x79, 0x14, 0x50, 0xf7, 0xf4,
-	0x78, 0x66, 0x12, 0xdb, 0x41, 0x22, 0x7b, 0x57, 0x55, 0x53, 0xf5, 0x75, 0xd5, 0x57, 0x87, 0x38,
-	0xb0, 0xdc, 0x27, 0xee, 0x0d, 0x0d, 0xbd, 0xce, 0x98, 0x33, 0xc9, 0xd0, 0xd2, 0x38, 0x98, 0x0c,
-	0xfc, 0xf0, 0xb6, 0xeb, 0xfc, 0x92, 0x83, 0xfa, 0xce, 0x78, 0x7c, 0x14, 0x0a, 0x49, 0x42, 0x97,
-	0xf6, 0xa8, 0x94, 0x7e, 0x38, 0x10, 0xa8, 0x09, 0x4b, 0x3f, 0x0a, 0x16, 0xee, 0x13, 0x49, 0x1a,
-	0xf9, 0x96, 0xd5, 0xae, 0xe2, 0xa9, 0x8e, 0x24, 0x3c, 0xf7, 0xa8, 0xcb, 0xef, 0xc6, 0x92, 0x7a,
-	0x3d, 0xea, 0x4e, 0x38, 0x3d, 0x8e, 0x5d, 0x0b, 0xad, 0x7c, 0xbb, 0xd2, 0xdd, 0xee, 0xc4, 0xf8,
-	0x9d, 0x19, 0xd8, 0x9d, 0xfd, 0xd9, 0xc1, 0x07, 0xa1, 0xe4, 0x77, 0x78, 0x1e, 0x34, 0xda, 0x84,
-	0xe5, 0x80, 0x08, 0x79, 0x35, 0xf6, 0x88, 0xa4, 0xde, 0x69, 0xaf, 0x51, 0x6c, 0x59, 0xed, 0x3c,
-	0xce, 0x1a, 0x9b, 0xc7, 0xb0, 0xbe, 0x08, 0x1e, 0xd5, 0x20, 0x7f, 0x43, 0xef, 0x1a, 0x56, 0xcb,
-	0x6a, 0xdb, 0x58, 0x89, 0x68, 0x15, 0x8a, 0xb7, 0x24, 0x98, 0xd0, 0x46, 0x4e, 0xdb, 0x22, 0x65,
-	0x3b, 0xf7, 0x85, 0xe5, 0xfc, 0x95, 0x87, 0xa6, 0x8a, 0xec, 0xb1, 0x09, 0x77, 0xe9, 0x03, 0x8a,
-	0x56, 0x20, 0xe7, 0x7b, 0x1a, 0x29, 0x8f, 0x73, 0xbe, 0x87, 0x10, 0x14, 0x42, 0x32, 0x8a, 0x71,
-	0xb4, 0xac, 0x9e, 0x9b, 0xf0, 0x40, 0x33, 0x68, 0x63, 0x25, 0x2a, 0xaf, 0x89, 0xa0, 0xbc, 0x51,
-	0x88, 0xbc, 0x94, 0xac, 0xc8, 0xf6, 0x88, 0x24, 0x7d, 0x22, 0xa8, 0xae, 0xca, 0xc6, 0x53, 0x1d,
-	0x6d, 0x41, 0xad, 0x4f, 0x84, 0xef, 0xee, 0x4c, 0xe4, 0xf0, 0x20, 0x24, 0xfd, 0x80, 0x7a, 0x8d,
-	0x52, 0xcb, 0x6a, 0x2f, 0xe1, 0x07, 0x76, 0x45, 0xd1, 0xd4, 0x76, 0xa5, 0x1e, 0x29, 0x6b, 0xb0,
-	0xac, 0x31, 0xd3, 0xda, 0xa5, 0x7b, 0xad, 0xfd, 0x69, 0x7e, 0x6b, 0x6d, 0xdd, 0xda, 0x9d, 0xa4,
-	0xb5, 0xf3, 0xa9, 0x79, 0xaa, 0x0e, 0xc3, 0xbb, 0xee, 0xf0, 0x2b, 0x28, 0x68, 0x4a, 0x56, 0xa1,
-	0x18, 0xb0, 0x81, 0x1f, 0x9a, 0xa8, 0x48, 0x99, 0xd9, 0xd0, 0x55, 0x28, 0xd2, 0x11, 0xf1, 0xe3,
-	0x96, 0x46, 0x8a, 0xf2, 0xe4, 0x2c, 0xa0, 0x71, 0x53, 0x95, 0xec, 0xfc, 0x9a, 0x83, 0xe5, 0x0b,
-	0xcd, 0xd5, 0x1e, 0x0b, 0x25, 0x7d, 0x23, 0x55, 0x2c, 0xe3, 0x83, 0xa3, 0x78, 0x66, 0x22, 0x45,
-	0xb5, 0x23, 0xa2, 0xf4, 0xc8, 0x33, 0x2f, 0x4d, 0x75, 0xe4, 0x98, 0x61, 0x51, 0x8f, 0x55, 0xba,
-	0x2b, 0x09, 0xf7, 0x2a, 0x6b, 0x33, 0x3c, 0xe7, 0x50, 0x27, 0x0f, 0x97, 0x4c, 0xa7, 0x52, 0xe9,
-	0x6e, 0x2c, 0xdc, 0x44, 0x3c, 0x2b, 0x12, 0x79, 0xd0, 0xf4, 0xe6, 0xb6, 0x56, 0xcf, 0x67, 0xa5,
-	0xbb, 0xf9, 0x6f, 0xc6, 0x00, 0x2f, 0xc0, 0x71, 0x36, 0x01, 0x7a, 0x92, 0xfb, 0xe1, 0xe0, 0xa5,
-	0x2f, 0x24, 0x5a, 0x83, 0x92, 0xee, 0x8a, 0x68, 0x58, 0xad, 0x7c, 0xdb, 0xc6, 0x46, 0x73, 0x7e,
-	0xcf, 0x41, 0x7d, 0x8f, 0x04, 0x01, 0xa6, 0x42, 0xc3, 0x60, 0xfa, 0x7a, 0x42, 0x85, 0x44, 0x5f,
-	0xc1, 0xf2, 0x38, 0xcd, 0xad, 0xa6, 0xb4, 0xd2, 0x7d, 0x9e, 0xa4, 0x95, 0xa1, 0x1e, 0x67, 0xbd,
-	0x55, 0xbf, 0xc6, 0x44, 0x0e, 0xe3, 0xce, 0x2a, 0x59, 0xa5, 0x30, 0xa2, 0x72, 0xc8, 0x3c, 0xd3,
-	0x5a, 0xa3, 0xc5, 0x2b, 0x5c, 0x48, 0x56, 0x78, 0x1f, 0xca, 0x43, 0x4a, 0x3c, 0xca, 0x15, 0x1b,
-	0x6a, 0x29, 0xb6, 0x92, 0x67, 0x67, 0x24, 0xdb, 0x39, 0x8c, 0x9c, 0xa3, 0xe9, 0x8f, 0x43, 0x55,
-	0x0e, 0x7d, 0xe6, 0xdd, 0xe9, 0x65, 0xae, 0x62, 0x2d, 0x37, 0x2f, 0xa0, 0x9a, 0x76, 0x9e, 0x31,
-	0xcb, 0x5b, 0xe9, 0x59, 0xae, 0x74, 0x57, 0x93, 0x97, 0x13, 0x36, 0xd3, 0x13, 0xfe, 0xa7, 0x05,
-	0xab, 0xd9, 0x9c, 0xc4, 0x98, 0x85, 0x82, 0xaa, 0xe7, 0x5d, 0xe6, 0x51, 0x8d, 0x5d, 0xc4, 0x5a,
-	0x46, 0x07, 0x49, 0x61, 0x39, 0x5d, 0xd8, 0x47, 0xf3, 0x0a, 0x8b, 0x40, 0x1e, 0xa9, 0x2c, 0xff,
-	0x4e, 0x2b, 0x3b, 0x01, 0xfb, 0xd2, 0x1f, 0x51, 0x4c, 0xc2, 0x01, 0x45, 0x2d, 0xa8, 0x5c, 0x73,
-	0x36, 0x3a, 0x18, 0x33, 0x77, 0x78, 0xda, 0x33, 0x0b, 0x96, 0x36, 0xa1, 0x75, 0xb0, 0x25, 0x8b,
-	0xbf, 0xe7, 0xf4, 0xf7, 0xc4, 0xe0, 0xbc, 0xb5, 0xc0, 0x56, 0x83, 0xfc, 0xf5, 0x84, 0x72, 0x7d,
-	0x30, 0x38, 0xbd, 0x36, 0x8b, 0x6a, 0xe3, 0x48, 0x51, 0xe7, 0x69, 0x44, 0xde, 0x28, 0xaf, 0x0b,
-	0xe6, 0x87, 0x52, 0x18, 0x94, 0xac, 0x11, 0xbd, 0x00, 0xf0, 0x43, 0x49, 0xf9, 0x2d, 0x09, 0x4e,
-	0x7b, 0x9a, 0x82, 0x3c, 0x4e, 0x59, 0xd0, 0x67, 0x60, 0xcb, 0x38, 0x6d, 0xb3, 0xa4, 0xf5, 0xa4,
-	0xd4, 0x69, 0x45, 0x38, 0xf1, 0x52, 0x7c, 0xaa, 0x03, 0xad, 0x57, 0xaf, 0x8a, 0xb5, 0xec, 0xfc,
-	0x6d, 0x41, 0x4d, 0x27, 0xab, 0x9e, 0x7e, 0xa2, 0xad, 0xd8, 0xb9, 0xdf, 0xfe, 0x0f, 0x92, 0xc0,
-	0xfb, 0x6f, 0xcd, 0x69, 0xfd, 0x27, 0x50, 0x7e, 0x3d, 0xa1, 0xdc, 0xa7, 0xa2, 0x91, 0xd7, 0x10,
-	0xf5, 0xec, 0xa1, 0xd0, 0x30, 0x38, 0xf6, 0x69, 0x6e, 0x3f, 0x3a, 0x15, 0xf3, 0x6f, 0xf7, 0x6f,
-	0x16, 0xfc, 0x3f, 0x95, 0x95, 0x19, 0xeb, 0x43, 0xb0, 0xb9, 0x91, 0xa3, 0x5b, 0x92, 0xd9, 0xce,
-	0x07, 0xfe, 0x9d, 0x58, 0x30, 0x85, 0x24, 0xc1, 0xcd, 0x4b, 0x58, 0xc9, 0x7e, 0x9c, 0x91, 0xdd,
-	0xc7, 0xd9, 0x99, 0x5d, 0xcb, 0x16, 0x1b, 0x87, 0xa7, 0xb3, 0xfe, 0x0e, 0xaa, 0x99, 0x7c, 0xd7,
-	0xa0, 0x74, 0xcd, 0xc9, 0xc8, 0x24, 0x5b, 0xc5, 0x46, 0xd3, 0x7f, 0x67, 0x38, 0x67, 0x3c, 0xae,
-	0x5b, 0x2b, 0xf1, 0x9f, 0xee, 0x53, 0x9a, 0xfd, 0x55, 0xa6, 0x74, 0xe7, 0x1b, 0x78, 0xb6, 0xc7,
-	0x82, 0x80, 0xba, 0xf2, 0x94, 0x4a, 0xee, 0xbb, 0xe2, 0x69, 0xa6, 0xc2, 0xf9, 0xd9, 0x82, 0xb5,
-	0xfb, 0xc0, 0x26, 0xf9, 0x5d, 0x28, 0x8f, 0x22, 0x93, 0xc1, 0x6c, 0xa7, 0xee, 0xc5, 0xcc, 0x90,
-	0xce, 0x05, 0xb9, 0x0b, 0x18, 0xf1, 0x70, 0x1c, 0xd8, 0xfc, 0x10, 0xca, 0xc6, 0xa6, 0x56, 0x67,
-	0xcc, 0x99, 0x3a, 0xbb, 0x74, 0x12, 0x21, 0x56, 0x71, 0xca, 0xe2, 0xf4, 0x00, 0xed, 0x0d, 0xa9,
-	0x7b, 0x73, 0x48, 0x49, 0x20, 0x87, 0x4f, 0x54, 0xde, 0x1f, 0x16, 0xd4, 0x33, 0xa8, 0xd3, 0xda,
-	0x4a, 0x42, 0x12, 0x69, 0x12, 0x59, 0xc9, 0xdc, 0xf8, 0x87, 0xee, 0x9d, 0x48, 0xed, 0xe9, 0x08,
-	0x6c, 0x22, 0x51, 0x43, 0xf1, 0x23, 0x04, 0x19, 0xc4, 0xe3, 0x1b, 0xab, 0xea, 0x5e, 0xe9, 0xdf,
-	0x5c, 0x54, 0x12, 0x3f, 0x10, 0xa6, 0x97, 0x69, 0x93, 0xd3, 0xd1, 0xab, 0x31, 0xc5, 0x44, 0x15,
-	0x28, 0x5f, 0x9d, 0x9d, 0x9c, 0x9d, 0x7f, 0x7b, 0x56, 0xfb, 0x1f, 0x2a, 0x41, 0xee, 0xfc, 0xa4,
-	0x66, 0x21, 0x1b, 0x8a, 0x07, 0x18, 0x9f, 0xe3, 0x5a, 0xae, 0xfb, 0x3d, 0x2c, 0xc5, 0xe7, 0x19,
-	0x9d, 0x43, 0x35, 0x7d, 0xae, 0xd1, 0xc6, 0xc2, 0xbf, 0x4f, 0xcd, 0x17, 0x8b, 0xaf, 0xfc, 0xa7,
-	0x56, 0xf7, 0x25, 0x14, 0xf4, 0x2f, 0xb4, 0x7d, 0xb0, 0xa7, 0x2b, 0x84, 0x9a, 0xf3, 0xaf, 0x43,
-	0xf3, 0xbd, 0x05, 0x3b, 0xd7, 0x7d, 0x6b, 0x41, 0x65, 0xdf, 0x27, 0x83, 0x90, 0x09, 0xe9, 0xbb,
-	0x02, 0x1d, 0x43, 0x25, 0x45, 0x29, 0x5a, 0x9f, 0xc3, 0x74, 0x84, 0xbc, 0xb1, 0xb0, 0x0f, 0xa8,
-	0x07, 0x2b, 0xd9, 0xc9, 0x43, 0xef, 0xcf, 0x9f, 0xc9, 0x08, 0xb1, 0xf5, 0xd8, 0xd0, 0x76, 0xaf,
-	0xc0, 0xbe, 0xe4, 0x24, 0x14, 0xd7, 0x8c, 0x8f, 0xd0, 0x21, 0x2c, 0x4f, 0x95, 0xff, 0xc6, 0xc3,
-	0x0f, 0xf0, 0x2c, 0x83, 0xa4, 0xc8, 0xdf, 0x25, 0xee, 0xcd, 0xd3, 0xd0, 0xbc, 0x5b, 0x7d, 0x05,
-	0x9d, 0x2f, 0xe3, 0xef, 0xfd, 0x92, 0xfe, 0xcf, 0xef, 0xf3, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff,
-	0x74, 0x8f, 0x41, 0x00, 0x0a, 0x0e, 0x00, 0x00,
+	// 1190 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x57, 0x4b, 0x6f, 0x23, 0xc5,
+	0x13, 0xff, 0x8f, 0x9f, 0x99, 0xb2, 0x13, 0xf9, 0xdf, 0xce, 0x66, 0x2d, 0xb3, 0xbb, 0x58, 0xa3,
+	0x48, 0x98, 0x00, 0x16, 0x0c, 0x17, 0x14, 0xc4, 0x21, 0x2f, 0x29, 0x8f, 0xcd, 0x83, 0x76, 0x02,
+	0x68, 0x11, 0x87, 0xf6, 0x4c, 0xc7, 0x1e, 0x32, 0x9e, 0x99, 0xed, 0x6e, 0x47, 0x6b, 0xf1, 0x01,
+	0xf8, 0x0c, 0x48, 0x7c, 0x06, 0x2e, 0xdc, 0xb9, 0x71, 0xe3, 0xce, 0x91, 0x8f, 0x02, 0xea, 0x9e,
+	0x1e, 0xcf, 0x4c, 0x62, 0x3b, 0x48, 0x64, 0x6f, 0x5d, 0x35, 0x55, 0xbf, 0xae, 0xfa, 0xd5, 0xa3,
+	0x6d, 0x58, 0x1d, 0x10, 0xe7, 0x86, 0x06, 0x6e, 0x2f, 0x62, 0xa1, 0x08, 0xd1, 0x4a, 0xe4, 0x4f,
+	0x86, 0x5e, 0x70, 0x6b, 0x5b, 0x3f, 0x15, 0xa0, 0xb9, 0x13, 0x45, 0x47, 0x01, 0x17, 0x24, 0x70,
+	0x68, 0x9f, 0x0a, 0xe1, 0x05, 0x43, 0x8e, 0xda, 0xb0, 0xf2, 0x3d, 0x0f, 0x83, 0x7d, 0x22, 0x48,
+	0xab, 0xd8, 0x31, 0xba, 0x75, 0x3c, 0x93, 0x91, 0x80, 0xa7, 0x2e, 0x75, 0xd8, 0x34, 0x12, 0xd4,
+	0xed, 0x53, 0x67, 0xc2, 0xe8, 0x71, 0x62, 0x5a, 0xea, 0x14, 0xbb, 0x35, 0x7b, 0xbb, 0x97, 0xe0,
+	0xf7, 0xe6, 0x60, 0xf7, 0xf6, 0xe7, 0x3b, 0x1f, 0x04, 0x82, 0x4d, 0xf1, 0x22, 0x68, 0xb4, 0x09,
+	0xab, 0x3e, 0xe1, 0xe2, 0x2a, 0x72, 0x89, 0xa0, 0xee, 0x69, 0xbf, 0x55, 0xee, 0x18, 0xdd, 0x22,
+	0xce, 0x2b, 0xdb, 0xc7, 0xf0, 0x6c, 0x19, 0x3c, 0x6a, 0x40, 0xf1, 0x86, 0x4e, 0x5b, 0x46, 0xc7,
+	0xe8, 0x9a, 0x58, 0x1e, 0xd1, 0x3a, 0x94, 0x6f, 0x89, 0x3f, 0xa1, 0xad, 0x82, 0xd2, 0xc5, 0xc2,
+	0x76, 0xe1, 0x33, 0xc3, 0xfa, 0xab, 0x08, 0x6d, 0xe9, 0xd9, 0x0f, 0x27, 0xcc, 0xa1, 0xf7, 0x28,
+	0x5a, 0x83, 0x82, 0xe7, 0x2a, 0xa4, 0x22, 0x2e, 0x78, 0x2e, 0x42, 0x50, 0x0a, 0xc8, 0x38, 0xc1,
+	0x51, 0x67, 0x79, 0xdd, 0x84, 0xf9, 0x8a, 0x41, 0x13, 0xcb, 0xa3, 0xb4, 0x9a, 0x70, 0xca, 0x5a,
+	0xa5, 0xd8, 0x4a, 0x9e, 0x25, 0xd9, 0x2e, 0x11, 0x64, 0x40, 0x38, 0x55, 0x59, 0x99, 0x78, 0x26,
+	0xa3, 0x2d, 0x68, 0x0c, 0x08, 0xf7, 0x9c, 0x9d, 0x89, 0x18, 0x1d, 0x04, 0x64, 0xe0, 0x53, 0xb7,
+	0x55, 0xe9, 0x18, 0xdd, 0x15, 0x7c, 0x4f, 0x2f, 0x29, 0x9a, 0xe9, 0xae, 0xe4, 0x25, 0x55, 0x05,
+	0x96, 0x57, 0xe6, 0x4a, 0xbb, 0x72, 0xa7, 0xb4, 0x3f, 0x2c, 0x2e, 0xad, 0xa9, 0x4a, 0xbb, 0x93,
+	0x96, 0x76, 0x31, 0x35, 0x8f, 0x55, 0x61, 0x78, 0xdb, 0x15, 0x7e, 0x05, 0x25, 0x45, 0xc9, 0x3a,
+	0x94, 0xfd, 0x70, 0xe8, 0x05, 0xda, 0x2b, 0x16, 0xe6, 0x16, 0x74, 0x1d, 0xca, 0x74, 0x4c, 0xbc,
+	0xa4, 0xa4, 0xb1, 0x20, 0x2d, 0x59, 0xe8, 0xd3, 0xa4, 0xa8, 0xf2, 0x6c, 0xfd, 0x5c, 0x80, 0xd5,
+	0x0b, 0xc5, 0xd5, 0x5e, 0x18, 0x08, 0xfa, 0x46, 0x48, 0xdf, 0x90, 0x0d, 0x8f, 0x92, 0x9e, 0x89,
+	0x05, 0x59, 0x8e, 0x98, 0xd2, 0x23, 0x57, 0xdf, 0x34, 0x93, 0x91, 0xa5, 0x9b, 0x45, 0x5e, 0x56,
+	0xb3, 0xd7, 0x52, 0xee, 0x65, 0xd4, 0xba, 0x79, 0xce, 0xa1, 0x49, 0xee, 0x0f, 0x99, 0x0a, 0xa5,
+	0x66, 0x3f, 0x5f, 0x3a, 0x89, 0x78, 0x9e, 0x27, 0x72, 0xa1, 0xed, 0x2e, 0x2c, 0xad, 0xea, 0xcf,
+	0x9a, 0xbd, 0xf9, 0x6f, 0xda, 0x00, 0x2f, 0xc1, 0xb1, 0x36, 0x01, 0xfa, 0x82, 0x79, 0xc1, 0xf0,
+	0xa5, 0xc7, 0x05, 0xda, 0x80, 0x8a, 0xaa, 0x0a, 0x6f, 0x19, 0x9d, 0x62, 0xd7, 0xc4, 0x5a, 0xb2,
+	0x7e, 0x2b, 0x40, 0x73, 0x8f, 0xf8, 0x3e, 0xa6, 0x5c, 0xc1, 0x60, 0xfa, 0x7a, 0x42, 0xb9, 0x40,
+	0x5f, 0xc0, 0x6a, 0x94, 0xe5, 0x56, 0x51, 0x5a, 0xb3, 0x9f, 0xa6, 0x61, 0xe5, 0xa8, 0xc7, 0x79,
+	0x6b, 0x59, 0xaf, 0x88, 0x88, 0x51, 0x52, 0x59, 0x79, 0x96, 0x21, 0x8c, 0xa9, 0x18, 0x85, 0xae,
+	0x2e, 0xad, 0x96, 0x92, 0x11, 0x2e, 0xa5, 0x23, 0xbc, 0x0f, 0xd5, 0x11, 0x25, 0x2e, 0x65, 0x92,
+	0x0d, 0x39, 0x14, 0x5b, 0xe9, 0xb5, 0x73, 0x82, 0xed, 0x1d, 0xc6, 0xc6, 0x71, 0xf7, 0x27, 0xae,
+	0x32, 0x86, 0x41, 0xe8, 0x4e, 0xd5, 0x30, 0xd7, 0xb1, 0x3a, 0xb7, 0x2f, 0xa0, 0x9e, 0x35, 0x9e,
+	0xd3, 0xcb, 0x5b, 0xd9, 0x5e, 0xae, 0xd9, 0xeb, 0xe9, 0xcd, 0x29, 0x9b, 0xd9, 0x0e, 0xff, 0xd3,
+	0x80, 0xf5, 0x7c, 0x4c, 0x3c, 0x0a, 0x03, 0x4e, 0xe5, 0xf5, 0x4e, 0xe8, 0x52, 0x85, 0x5d, 0xc6,
+	0xea, 0x8c, 0x0e, 0xd2, 0xc4, 0x0a, 0x2a, 0xb1, 0x0f, 0x16, 0x25, 0x16, 0x83, 0x3c, 0x90, 0x59,
+	0xf1, 0xad, 0x66, 0x76, 0x02, 0xe6, 0xa5, 0x37, 0xa6, 0x98, 0x04, 0x43, 0x8a, 0x3a, 0x50, 0xbb,
+	0x66, 0xe1, 0xf8, 0x20, 0x0a, 0x9d, 0xd1, 0x69, 0x5f, 0x0f, 0x58, 0x56, 0x85, 0x9e, 0x81, 0x29,
+	0xc2, 0xe4, 0x7b, 0x41, 0x7d, 0x4f, 0x15, 0xd6, 0x1f, 0x06, 0x98, 0xb2, 0x91, 0xbf, 0x9c, 0x50,
+	0xa6, 0x16, 0x06, 0xa3, 0xd7, 0x7a, 0x50, 0x4d, 0x1c, 0x0b, 0x72, 0x3d, 0x8d, 0xc9, 0x1b, 0x69,
+	0x75, 0x11, 0x7a, 0x81, 0xe0, 0x1a, 0x25, 0xaf, 0x44, 0x2f, 0x00, 0xbc, 0x40, 0x50, 0x76, 0x4b,
+	0xfc, 0xd3, 0xbe, 0xa2, 0xa0, 0x88, 0x33, 0x1a, 0xf4, 0x09, 0x98, 0x22, 0x09, 0x5b, 0x0f, 0x69,
+	0x33, 0x4d, 0x75, 0x96, 0x11, 0x4e, 0xad, 0x24, 0x9f, 0x72, 0x41, 0xab, 0xd1, 0xab, 0x63, 0x75,
+	0x96, 0xe9, 0xbc, 0x96, 0xb1, 0x5e, 0x4e, 0x23, 0xaa, 0x5a, 0xc8, 0xc4, 0xa9, 0xc2, 0xfa, 0xdb,
+	0x80, 0x86, 0x4a, 0x45, 0x06, 0xf6, 0x48, 0x33, 0xb3, 0x73, 0xb7, 0x39, 0xde, 0x4b, 0x1d, 0xef,
+	0xde, 0xb5, 0xa0, 0x31, 0x3e, 0x82, 0xaa, 0x8c, 0xd1, 0xa3, 0xbc, 0x55, 0x54, 0x10, 0xcd, 0xfc,
+	0x1a, 0x51, 0x30, 0x38, 0xb1, 0x69, 0x6f, 0x3f, 0xd8, 0x33, 0x8b, 0x37, 0xfb, 0xaf, 0x06, 0xfc,
+	0x3f, 0x13, 0x95, 0x6e, 0xfa, 0x43, 0x30, 0x99, 0x3e, 0xc7, 0x9b, 0x26, 0x37, 0xbb, 0xf7, 0xec,
+	0x7b, 0xc9, 0x41, 0x27, 0x92, 0x3a, 0xb7, 0x2f, 0x61, 0x2d, 0xff, 0x71, 0x4e, 0x74, 0x1f, 0xe6,
+	0x3b, 0x7a, 0x23, 0x9f, 0x6c, 0xe2, 0x9e, 0x8d, 0xfa, 0x1b, 0xa8, 0xe7, 0xe2, 0xdd, 0x80, 0xca,
+	0x35, 0x23, 0x63, 0x1d, 0x6c, 0x1d, 0x6b, 0x49, 0xbd, 0x42, 0x8c, 0x85, 0x2c, 0xc9, 0x5b, 0x09,
+	0xc9, 0xc3, 0x7e, 0x4a, 0xf3, 0xbf, 0xd9, 0xa4, 0x6c, 0x7d, 0x05, 0x4f, 0xf6, 0x42, 0xdf, 0xa7,
+	0x8e, 0x38, 0xa5, 0x82, 0x79, 0x0e, 0x7f, 0x9c, 0xae, 0xb0, 0x7e, 0x34, 0x60, 0xe3, 0x2e, 0xb0,
+	0x0e, 0x7e, 0x17, 0xaa, 0xe3, 0x58, 0xa5, 0x31, 0xbb, 0x99, 0x6d, 0x32, 0xd7, 0xa5, 0x77, 0x41,
+	0xa6, 0x7e, 0x48, 0x5c, 0x9c, 0x38, 0xb6, 0xdf, 0x87, 0xaa, 0xd6, 0xc9, 0xc1, 0x8a, 0x58, 0x28,
+	0x97, 0x32, 0x9d, 0xc4, 0x88, 0x75, 0x9c, 0xd1, 0x58, 0x7d, 0x40, 0x7b, 0x23, 0xea, 0xdc, 0x1c,
+	0x52, 0xe2, 0x8b, 0xd1, 0x23, 0xa5, 0xf7, 0xbb, 0x01, 0xcd, 0x1c, 0xea, 0x2c, 0xb7, 0x0a, 0x17,
+	0x44, 0xe8, 0x40, 0xd6, 0x72, 0x2f, 0xc0, 0x7d, 0xf3, 0x5e, 0x2c, 0xf6, 0x95, 0x07, 0xd6, 0x9e,
+	0xa8, 0x25, 0xf9, 0xe1, 0x9c, 0x0c, 0x93, 0xf6, 0x4d, 0x44, 0xb9, 0xcd, 0xd4, 0x2f, 0x32, 0x2a,
+	0x88, 0xe7, 0x73, 0x5d, 0xcb, 0xac, 0xca, 0xea, 0xa9, 0xd1, 0x98, 0x61, 0xa2, 0x1a, 0x54, 0xaf,
+	0xce, 0x4e, 0xce, 0xce, 0xbf, 0x3e, 0x6b, 0xfc, 0x0f, 0x55, 0xa0, 0x70, 0x7e, 0xd2, 0x30, 0x90,
+	0x09, 0xe5, 0x03, 0x8c, 0xcf, 0x71, 0xa3, 0x60, 0x7f, 0x0b, 0x2b, 0xc9, 0xf2, 0x46, 0xe7, 0x50,
+	0xcf, 0x2e, 0x73, 0xf4, 0x7c, 0xe9, 0xeb, 0xd5, 0x7e, 0xb1, 0xfc, 0x0d, 0xf8, 0xd8, 0xb0, 0x5f,
+	0x42, 0x49, 0xfd, 0x7e, 0xdb, 0x07, 0x73, 0x36, 0x42, 0xa8, 0xbd, 0x78, 0x3b, 0xb4, 0xdf, 0x59,
+	0x32, 0x73, 0xf6, 0x2f, 0x06, 0xd4, 0xf6, 0x3d, 0x32, 0x0c, 0x42, 0x2e, 0x3c, 0x87, 0xa3, 0x63,
+	0xa8, 0x65, 0x28, 0x45, 0xcf, 0x16, 0x30, 0x1d, 0x23, 0x3f, 0x5f, 0x5a, 0x07, 0xd4, 0x87, 0xb5,
+	0x7c, 0xe7, 0xa1, 0x77, 0x17, 0xf7, 0x64, 0x8c, 0xd8, 0x79, 0xa8, 0x69, 0xed, 0x2b, 0x30, 0x2f,
+	0x19, 0x09, 0xf8, 0x75, 0xc8, 0xc6, 0xe8, 0x10, 0x56, 0x67, 0xc2, 0x7f, 0xe3, 0xe1, 0x3b, 0x78,
+	0x92, 0x43, 0x92, 0xe4, 0xef, 0x12, 0xe7, 0xe6, 0x71, 0x68, 0xde, 0xad, 0xbf, 0x82, 0xde, 0xe7,
+	0xc9, 0xf7, 0x41, 0x45, 0xfd, 0x2f, 0xfc, 0xf4, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x2f, 0x3a,
+	0xd4, 0x1a, 0x28, 0x0e, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
diff --git a/go.mod b/go.mod
index 10ea5f3..5ebeaf3 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,6 @@ go 1.14
 require (
 	github.com/apache/arrow/go/arrow v0.0.0-20200403134915-89ce1cadb678
 	github.com/cheekybits/genny v1.0.0
-	github.com/davecgh/go-spew v1.1.1
 	github.com/golang/protobuf v1.3.4
 	github.com/google/go-cmp v0.3.1
 	github.com/grpc-ecosystem/go-grpc-middleware v1.2.0
diff --git a/go.sum b/go.sum
index 7af82cd..225dd37 100644
--- a/go.sum
+++ b/go.sum
@@ -113,6 +113,7 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
diff --git a/proto/backend.proto b/proto/backend.proto
index 18655f9..d5bb555 100644
--- a/proto/backend.proto
+++ b/proto/backend.proto
@@ -106,6 +106,7 @@ message DataQuery {
   int64 intervalMS = 3;
   TimeRange timeRange = 4;
   bytes json = 5;
+  string queryType = 6;
 }
 
 // QueryDataRequest


Test patch:
diff --git a/backend/convert_from_protobuf_test.go b/backend/convert_from_protobuf_test.go
index 893cf7d..1486387 100644
--- a/backend/convert_from_protobuf_test.go
+++ b/backend/convert_from_protobuf_test.go
@@ -7,7 +7,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/davecgh/go-spew/spew"
 	"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
 	"github.com/mitchellh/reflectwalk"
 	"github.com/stretchr/testify/require"
@@ -30,7 +29,6 @@ func (w *walker) StructField(f reflect.StructField, v reflect.Value) error {
 	if f.PkgPath != "" {
 		return nil
 	}
-	spew.Dump(f)
 	w.FieldCount++
 	if v.IsZero() {
 		w.ZeroValueFieldCount++
@@ -304,6 +302,7 @@ var protoDataQuery = &pluginv2.DataQuery{
 	TimeRange:     protoTimeRange,
 	IntervalMS:    60 * 1000,
 	Json:          []byte(`{ "query": "SELECT * from FUN"`),
+	QueryType:     "qt",
 }
 
 func TestConvertFromProtobufDataQuery(t *testing.T) {
@@ -334,6 +333,8 @@ func TestConvertFromProtobufDataQuery(t *testing.T) {
 
 	requireCounter.Equal(t, protoDQ.RefId, sdkDQ.RefID)
 	requireCounter.Equal(t, protoDQ.MaxDataPoints, sdkDQ.MaxDataPoints)
+	requireCounter.Equal(t, protoDQ.QueryType, sdkDQ.QueryType)
+
 	requireCounter.Equal(t, time.Duration(time.Minute), sdkDQ.Interval)
 	requireCounter.Equal(t, sdkTimeRange.From, sdkDQ.TimeRange.From)
 	requireCounter.Equal(t, sdkTimeRange.To, sdkDQ.TimeRange.To)
@@ -406,6 +407,7 @@ func TestConvertFromProtobufQueryDataRequest(t *testing.T) {
 	// Queries
 	requireCounter.Equal(t, protoQDR.Queries[0].RefId, sdkQDR.Queries[0].RefID)
 	requireCounter.Equal(t, protoQDR.Queries[0].MaxDataPoints, sdkQDR.Queries[0].MaxDataPoints)
+	requireCounter.Equal(t, protoQDR.Queries[0].QueryType, sdkQDR.Queries[0].QueryType)
 	requireCounter.Equal(t, time.Duration(time.Minute), sdkQDR.Queries[0].Interval)
 	requireCounter.Equal(t, sdkTimeRange.From, sdkQDR.Queries[0].TimeRange.From)
 	requireCounter.Equal(t, sdkTimeRange.To, sdkQDR.Queries[0].TimeRange.To)
diff --git a/backend/datasource/query_type_mux_test.go b/backend/datasource/query_type_mux_test.go
new file mode 100644
index 0000000..ccf797e
--- /dev/null
+++ b/backend/datasource/query_type_mux_test.go
@@ -0,0 +1,104 @@
+package datasource
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"github.com/grafana/grafana-plugin-sdk-go/backend"
+	"github.com/stretchr/testify/require"
+)
+
+func ExampleQueryTypeMux() {
+	mux := NewQueryTypeMux()
+	mux.HandleFunc("queryTypeA", func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
+		// handle queryTypeA
+		return nil, nil
+	})
+	mux.HandleFunc("queryTypeB", func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
+		// handle queryTypeB
+		return nil, nil
+	})
+
+	_ = backend.ServeOpts{
+		QueryDataHandler: mux,
+	}
+}
+
+func TestQueryTypeMux(t *testing.T) {
+	mux := NewQueryTypeMux()
+	aHandler := &testHandler{}
+	mux.Handle("a", aHandler)
+	bHandler := &testHandler{}
+	mux.Handle("b", bHandler)
+
+	res, err := mux.QueryData(context.Background(), &backend.QueryDataRequest{
+		Queries: []backend.DataQuery{
+			backend.DataQuery{
+				RefID:     "A",
+				QueryType: "a",
+			},
+			backend.DataQuery{
+				RefID:     "B",
+				QueryType: "b",
+			},
+			backend.DataQuery{
+				RefID:     "C",
+				QueryType: "a",
+			},
+			backend.DataQuery{
+				RefID:     "D",
+				QueryType: "d",
+			},
+		},
+	})
+
+	require.NoError(t, err)
+	require.Equal(t, 1, aHandler.callCount)
+	require.Len(t, aHandler.request.Queries, 2)
+	require.Equal(t, "A", aHandler.request.Queries[0].RefID)
+	require.Equal(t, "C", aHandler.request.Queries[1].RefID)
+
+	require.Equal(t, 1, bHandler.callCount)
+	require.Len(t, bHandler.request.Queries, 1)
+	require.Equal(t, "B", bHandler.request.Queries[0].RefID)
+	require.Len(t, res.Responses, 4)
+	require.Equal(t, "no handler found for query type 'd'", res.Responses["D"].Error.Error())
+
+	t.Run("When overriding fallback handler should call fallback handler", func(t *testing.T) {
+		errBoom := errors.New("BOOM")
+		mux.HandleFunc("", func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
+			return nil, errBoom
+		})
+		res, err = mux.QueryData(context.Background(), &backend.QueryDataRequest{
+			Queries: []backend.DataQuery{
+				backend.DataQuery{
+					RefID:     "A",
+					QueryType: "unhandled",
+				},
+			},
+		})
+
+		require.Nil(t, res)
+		require.Error(t, err)
+		require.Equal(t, errBoom, err)
+	})
+}
+
+type testHandler struct {
+	callCount int
+	request   *backend.QueryDataRequest
+}
+
+func (th *testHandler) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
+	th.callCount++
+	th.request = req
+	responses := backend.Responses{}
+	for _, q := range req.Queries {
+		responses[q.RefID] = backend.DataResponse{}
+	}
+
+	return &backend.QueryDataResponse{
+		Responses: responses,
+	}, nil
+}
diff --git a/data/frame_meta_test.go b/data/frame_meta_test.go
new file mode 100644
index 0000000..64f6f5a
--- /dev/null
+++ b/data/frame_meta_test.go
@@ -0,0 +1,37 @@
+package data
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestJSONNotice(t *testing.T) {
+	tests := []struct {
+		name   string
+		notice Notice
+		json   string
+	}{
+		{
+			name: "notice with severity and text",
+			notice: Notice{
+				Severity: NoticeSeverityError,
+				Text:     "Some text",
+			},
+			json: `{"severity":"error","text":"Some text"}`,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			b, err := json.Marshal(tt.notice)
+			require.NoError(t, err)
+			require.Equal(t, tt.json, string(b))
+
+			n := Notice{}
+			err = json.Unmarshal([]byte(tt.json), &n)
+			require.NoError(t, err)
+			require.Equal(t, tt.notice, n)
+		})
+	}
+}