From 05fd7eb047607ab1fe25f8a7d21b27ed0da36794 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Thu, 14 Jul 2022 09:18:12 -0400 Subject: [PATCH] SSE: Add noData type (#51973) When there is a single frame with no fields (e.g. splunk datasource) SSE errors when trying to figure out the data type. This frame needs to exist since this is where the executedQueryString metadata exists. This adds a new return type to SSE to represent no data, so the original frame with its metadata can still be maintained. --- pkg/expr/mathexp/exp.go | 11 ++++++++- pkg/expr/mathexp/parse/node.go | 4 ++++ pkg/expr/mathexp/types.go | 41 ++++++++++++++++++++++++++++++++++ pkg/expr/mathexp/union_test.go | 26 +++++++++++++++++++++ pkg/expr/nodes.go | 8 ++++++- 5 files changed, 88 insertions(+), 2 deletions(-) diff --git a/pkg/expr/mathexp/exp.go b/pkg/expr/mathexp/exp.go index afb00c3a74f..b772969d371 100644 --- a/pkg/expr/mathexp/exp.go +++ b/pkg/expr/mathexp/exp.go @@ -117,6 +117,8 @@ func (e *State) walkUnary(node *parse.UnaryNode) (Results, error) { newVal, err = e.unaryNumber(rt, node.OpStr) case Series: newVal, err = e.unarySeries(rt, node.OpStr) + case NoData: + newVal = NoData{}.New() default: return newResults, fmt.Errorf("can not perform a unary operation on type %v", rt.Type()) } @@ -192,9 +194,16 @@ type Union struct { // number of tags. func union(aResults, bResults Results) []*Union { unions := []*Union{} - if len(aResults.Values) == 0 || len(bResults.Values) == 0 { + aValueLen := len(aResults.Values) + bValueLen := len(bResults.Values) + if aValueLen == 0 || bValueLen == 0 { return unions } + if aValueLen == 1 || bValueLen == 1 { + if aResults.Values[0].Type() == parse.TypeNoData || bResults.Values[0].Type() == parse.TypeNoData { + return unions + } + } for _, a := range aResults.Values { for _, b := range bResults.Values { var labels data.Labels diff --git a/pkg/expr/mathexp/parse/node.go b/pkg/expr/mathexp/parse/node.go index 711d0bf65e9..98a006e6343 100644 --- a/pkg/expr/mathexp/parse/node.go +++ b/pkg/expr/mathexp/parse/node.go @@ -401,6 +401,8 @@ const ( TypeSeriesSet // TypeVariantSet is a collection of the same type Number, Series, or Scalar. TypeVariantSet + // TypeNoData is a no data response without a known data type. + TypeNoData ) // String returns a string representation of the ReturnType. @@ -416,6 +418,8 @@ func (f ReturnType) String() string { return "scalar" case TypeVariantSet: return "variant" + case TypeNoData: + return "noData" default: return "unknown" } diff --git a/pkg/expr/mathexp/types.go b/pkg/expr/mathexp/types.go index 299fe47b84b..3e020d85d5a 100644 --- a/pkg/expr/mathexp/types.go +++ b/pkg/expr/mathexp/types.go @@ -173,3 +173,44 @@ func (ff *Float64Field) Len() int { df := data.Field(*ff) return df.Len() } + +// NoData is an untyped no data response. +type NoData struct{ Frame *data.Frame } + +// Type returns the Value type and allows it to fulfill the Value interface. +func (s NoData) Type() parse.ReturnType { return parse.TypeNoData } + +// Value returns the actual value allows it to fulfill the Value interface. +func (s NoData) Value() interface{} { return s } + +func (s NoData) GetLabels() data.Labels { return nil } + +func (s NoData) SetLabels(ls data.Labels) {} + +func (s NoData) GetMeta() interface{} { + return s.Frame.Meta.Custom +} + +func (s NoData) SetMeta(v interface{}) { + m := s.Frame.Meta + if m == nil { + m = &data.FrameMeta{} + s.Frame.SetMeta(m) + } + m.Custom = v +} + +func (s NoData) AddNotice(notice data.Notice) { + m := s.Frame.Meta + if m == nil { + m = &data.FrameMeta{} + s.Frame.SetMeta(m) + } + m.Notices = append(m.Notices, notice) +} + +func (s NoData) AsDataFrame() *data.Frame { return s.Frame } + +func (s NoData) New() NoData { + return NoData{data.NewFrame("no data")} +} diff --git a/pkg/expr/mathexp/union_test.go b/pkg/expr/mathexp/union_test.go index f46f7672185..da1c4ff5aa6 100644 --- a/pkg/expr/mathexp/union_test.go +++ b/pkg/expr/mathexp/union_test.go @@ -79,6 +79,32 @@ func Test_union(t *testing.T) { unionsAre: assert.EqualValues, unions: []*Union{}, }, + { + name: "empty result and data result will result in no unions", + aResults: Results{ + Values: Values{ + makeSeries("a", data.Labels{"id": "1"}), + }, + }, + bResults: Results{}, + unionsAre: assert.EqualValues, + unions: []*Union{}, + }, + { + name: "no data result and data result will result in no unions", + aResults: Results{ + Values: Values{ + makeSeries("a", data.Labels{"id": "1"}), + }, + }, + bResults: Results{ + Values: Values{ + NoData{}.New(), + }, + }, + unionsAre: assert.EqualValues, + unions: []*Union{}, + }, { name: "incompatible tags of different length with will result in no unions when len(A) != 1 && len(B) != 1", aResults: Results{ diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index 94f45613c86..4c49bf133ef 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -237,7 +237,7 @@ func (dn *DSNode) Execute(ctx context.Context, vars mathexp.Vars, s *Service) (m } dataSource := dn.datasource.Type - if isAllFrameVectors(dataSource, qr.Frames) { + if isAllFrameVectors(dataSource, qr.Frames) { // Prometheus Specific Handling vals, err = framesToNumbers(qr.Frames) if err != nil { return mathexp.Results{}, fmt.Errorf("failed to read frames as numbers: %w", err) @@ -247,6 +247,12 @@ func (dn *DSNode) Execute(ctx context.Context, vars mathexp.Vars, s *Service) (m if len(qr.Frames) == 1 { frame := qr.Frames[0] + // Handle Untyped NoData + if len(frame.Fields) == 0 { + return mathexp.Results{Values: mathexp.Values{mathexp.NoData{Frame: frame}}}, nil + } + + // Handle Numeric Table if frame.TimeSeriesSchema().Type == data.TimeSeriesTypeNot && isNumberTable(frame) { logger.Debug("expression datasource query (numberSet)", "query", refID) numberSet, err := extractNumberSet(frame)