diff --git a/storage/labelvalues_test.go b/storage/labelvalues_test.go index a3cfe3e0b..efbc58d55 100644 --- a/storage/labelvalues_test.go +++ b/storage/labelvalues_test.go @@ -10,7 +10,7 @@ import ( func TestListLabelValues(t *testing.T) { t.Run("lets you traverse a slice of label values", func(t *testing.T) { input := []string{"a", "b", "c", "d"} - it := NewListLabelValues(input) + it := NewListLabelValues(input, nil) t.Cleanup(func() { require.NoError(t, it.Close()) }) @@ -25,7 +25,7 @@ func TestListLabelValues(t *testing.T) { }) t.Run("can be initialized with an empty slice", func(t *testing.T) { - it := NewListLabelValues([]string{}) + it := NewListLabelValues([]string{}, nil) t.Cleanup(func() { require.NoError(t, it.Close()) }) @@ -36,7 +36,7 @@ func TestListLabelValues(t *testing.T) { }) t.Run("can be initialized with a nil slice", func(t *testing.T) { - it := NewListLabelValues(nil) + it := NewListLabelValues(nil, nil) t.Cleanup(func() { require.NoError(t, it.Close()) }) diff --git a/tsdb/block_test.go b/tsdb/block_test.go index 157e32bfb..65d37bab7 100644 --- a/tsdb/block_test.go +++ b/tsdb/block_test.go @@ -314,6 +314,10 @@ func TestLabelValuesStream_WithMatchers(t *testing.T) { "unique", fmt.Sprintf("value%d", i), ), []chunks.Sample{sample{100, 0, nil, nil}})) } + // Add another series with an overlapping unique label, but leaving out the tens label + seriesEntries = append(seriesEntries, storage.NewListSeries(labels.FromStrings( + "unique", "value99", + ), []chunks.Sample{sample{100, 0, nil, nil}})) ctx := context.Background() @@ -371,6 +375,15 @@ func TestLabelValuesStream_WithMatchers(t *testing.T) { labels.MustNewMatcher(labels.MatchNotEqual, "tens", ""), }, expectedValues: uniqueWithout30s, + }, { + // In this case, we query for the "unique" label where the "tens" label is absent. + // We have one series where "tens" is empty (unique="value99"), but also another with + // the same value for "unique" and "tens" present. Make sure that unique="value99" is + // still found. + name: "get unique ID where tens is empty", + labelName: "unique", + matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "tens", "")}, + expectedValues: []string{"value99"}, }, } for _, tt := range testCases { diff --git a/tsdb/head_test.go b/tsdb/head_test.go index 030430a92..14085f9c5 100644 --- a/tsdb/head_test.go +++ b/tsdb/head_test.go @@ -2851,6 +2851,11 @@ func TestHeadLabelValuesStream_WithMatchers(t *testing.T) { ), 100, 0) require.NoError(t, err) } + // Add another series with an overlapping unique label, but leaving out the tens label + _, err := app.Append(0, labels.FromStrings( + "unique", "value99", + ), 100, 0) + require.NoError(t, err) require.NoError(t, app.Commit()) indexReader := head.indexRange(0, 200) @@ -2904,6 +2909,15 @@ func TestHeadLabelValuesStream_WithMatchers(t *testing.T) { labelName: "tens", matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "unique", "")}, expectedValues: nil, + }, { + // In this case, we query for the "unique" label where the "tens" label is absent. + // We have one series where "tens" is empty (unique="value99"), but also another with + // the same value for "unique" and "tens" present. Make sure that unique="value99" is + // still found. + name: "get unique ID where tens is empty", + labelName: "unique", + matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "tens", "")}, + expectedValues: []string{"value99"}, }, } for _, tt := range testCases { diff --git a/tsdb/index/labelvalues.go b/tsdb/index/labelvalues.go index 88c8c4e9f..1becd58bd 100644 --- a/tsdb/index/labelvalues.go +++ b/tsdb/index/labelvalues.go @@ -206,7 +206,7 @@ func (r *Reader) labelValuesForV1(postings Postings, name string, includeMatches slices.Sort(vals) return &intersectLabelValuesV1{ postingOffsets: e, - values: storage.NewListLabelValues(vals), + values: storage.NewListLabelValues(vals, nil), postings: NewPostingsCloner(postings), b: r.b, dec: r.dec, @@ -494,7 +494,40 @@ func (p *MemPostings) labelValuesFor(postings Postings, name string, includeMatc } slices.Sort(vals) - return storage.NewListLabelValues(vals) + return storage.NewListLabelValues(vals, nil) +} + +// LabelValuesFromSeries returns all unique label values from r for given label name from supplied series. Values are not sorted. +// buf is space for holding the result (if it isn't big enough, it will be ignored), may be nil. +func LabelValuesFromSeries(lg LabelsGetter, labelName string, refs []storage.SeriesRef, buf []string) ([]string, error) { + values := make(map[string]struct{}, len(buf)) + + var builder labels.ScratchBuilder + for _, ref := range refs { + err := lg.Labels(ref, &builder) + // Postings may be stale. Skip if no underlying series exists. + if errors.Is(err, storage.ErrNotFound) { + continue + } + if err != nil { + return nil, fmt.Errorf("label values for label %s: %w", labelName, err) + } + + v := builder.Labels().Get(labelName) + if v != "" { + values[v] = struct{}{} + } + } + + if cap(buf) >= len(values) { + buf = buf[:0] + } else { + buf = make([]string, 0, len(values)) + } + for v := range values { + buf = append(buf, v) + } + return buf, nil } // intersect returns whether p1 and p2 have at least one series in common. @@ -598,4 +631,3 @@ func (p *MemPostings) PostingsForRegexp(ctx context.Context, m *labels.Matcher) return Merge(ctx, its...) } - diff --git a/tsdb/index/labelvalues_test.go b/tsdb/index/labelvalues_test.go index 40f3c8ee7..a4edfcd20 100644 --- a/tsdb/index/labelvalues_test.go +++ b/tsdb/index/labelvalues_test.go @@ -230,7 +230,7 @@ func TestMemPostings_LabelValuesFor(t *testing.T) { t.Run("filtering based on non-empty postings", func(t *testing.T) { p := mp.Get("a", "1") - it := mp.LabelValuesFor(p, "b") + it := mp.LabelValuesFor(p, "b", nil) t.Cleanup(func() { require.NoError(t, it.Close()) }) @@ -248,7 +248,7 @@ func TestMemPostings_LabelValuesFor(t *testing.T) { t.Run("requesting a non-existent label value", func(t *testing.T) { p := mp.Get("a", "1") - it := mp.LabelValuesFor(p, "c") + it := mp.LabelValuesFor(p, "c", nil) t.Cleanup(func() { require.NoError(t, it.Close()) }) @@ -259,7 +259,7 @@ func TestMemPostings_LabelValuesFor(t *testing.T) { }) t.Run("filtering based on empty postings", func(t *testing.T) { - it := mp.LabelValuesFor(EmptyPostings(), "a") + it := mp.LabelValuesFor(EmptyPostings(), "a", nil) t.Cleanup(func() { require.NoError(t, it.Close()) }) @@ -272,7 +272,7 @@ func TestMemPostings_LabelValuesFor(t *testing.T) { t.Run("filtering based on a postings set missing the label", func(t *testing.T) { p := mp.Get("d", "1") - it := mp.LabelValuesFor(p, "a") + it := mp.LabelValuesFor(p, "a", nil) t.Cleanup(func() { require.NoError(t, it.Close()) })