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)