Query oracle and API freeze
Label matchers (equal, not-equal, regexp, not-regexp) in labels/, postings intersection/union/difference in internal/postings/, LabelValues and AllPostings on index, block, and head readers. ingot.go wired to real DB backed by head + block readers. Querier snapshots overlapping blocks, Select resolves matchers to postings, merged iterator chains blocks then head with block-wins dedup. Oracle tests compare every query against a naive []sample reference across head-only, block-only, head+block merge seam, multiple blocks, all four matcher types, and time range filtering.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package labels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// MatchType identifies the type of a label matcher.
|
||||
type MatchType int
|
||||
|
||||
const (
|
||||
MatchEqual MatchType = iota
|
||||
MatchNotEqual
|
||||
MatchRegexp
|
||||
MatchNotRegexp
|
||||
)
|
||||
|
||||
func (m MatchType) String() string {
|
||||
switch m {
|
||||
case MatchEqual:
|
||||
return "="
|
||||
case MatchNotEqual:
|
||||
return "!="
|
||||
case MatchRegexp:
|
||||
return "=~"
|
||||
case MatchNotRegexp:
|
||||
return "!~"
|
||||
default:
|
||||
return "??"
|
||||
}
|
||||
}
|
||||
|
||||
// Matcher matches label values against a pattern.
|
||||
type Matcher struct {
|
||||
Type MatchType
|
||||
Name string
|
||||
Value string
|
||||
re *regexp.Regexp // compiled for MatchRegexp/MatchNotRegexp
|
||||
}
|
||||
|
||||
// NewMatcher creates a new Matcher. For regex types, the value is compiled
|
||||
// as a full-match regular expression (anchored with ^(?:...)$).
|
||||
func NewMatcher(typ MatchType, name, value string) (*Matcher, error) {
|
||||
m := &Matcher{Type: typ, Name: name, Value: value}
|
||||
if typ == MatchRegexp || typ == MatchNotRegexp {
|
||||
re, err := regexp.Compile("^(?:" + value + ")$")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("labels: bad matcher regex %q: %w", value, err)
|
||||
}
|
||||
m.re = re
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// MustNewMatcher is like NewMatcher but panics on error.
|
||||
func MustNewMatcher(typ MatchType, name, value string) *Matcher {
|
||||
m, err := NewMatcher(typ, name, value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Matches reports whether the given label value satisfies this matcher.
|
||||
func (m *Matcher) Matches(v string) bool {
|
||||
switch m.Type {
|
||||
case MatchEqual:
|
||||
return v == m.Value
|
||||
case MatchNotEqual:
|
||||
return v != m.Value
|
||||
case MatchRegexp:
|
||||
return m.re.MatchString(v)
|
||||
case MatchNotRegexp:
|
||||
return !m.re.MatchString(v)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Matcher) String() string {
|
||||
return fmt.Sprintf("%s%s%q", m.Name, m.Type, m.Value)
|
||||
}
|
||||
|
||||
// FromStrings creates a sorted label set from alternating name/value pairs.
|
||||
// Panics if an odd number of strings is provided.
|
||||
func FromStrings(ss ...string) []Label {
|
||||
if len(ss)%2 != 0 {
|
||||
panic("labels.FromStrings: odd number of arguments")
|
||||
}
|
||||
ls := make([]Label, len(ss)/2)
|
||||
for i := 0; i < len(ss); i += 2 {
|
||||
ls[i/2] = Label{Name: ss[i], Value: ss[i+1]}
|
||||
}
|
||||
return Sort(ls)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package labels
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMatcherMatches(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
typ MatchType
|
||||
pattern string
|
||||
value string
|
||||
want bool
|
||||
}{
|
||||
// MatchEqual
|
||||
{name: "equal_match", typ: MatchEqual, pattern: "foo", value: "foo", want: true},
|
||||
{name: "equal_no_match", typ: MatchEqual, pattern: "foo", value: "bar", want: false},
|
||||
{name: "equal_empty", typ: MatchEqual, pattern: "", value: "", want: true},
|
||||
{name: "equal_empty_vs_nonempty", typ: MatchEqual, pattern: "", value: "x", want: false},
|
||||
|
||||
// MatchNotEqual
|
||||
{name: "not_equal_match", typ: MatchNotEqual, pattern: "foo", value: "bar", want: true},
|
||||
{name: "not_equal_no_match", typ: MatchNotEqual, pattern: "foo", value: "foo", want: false},
|
||||
{name: "not_equal_empty", typ: MatchNotEqual, pattern: "", value: "x", want: true},
|
||||
|
||||
// MatchRegexp
|
||||
{name: "regexp_exact", typ: MatchRegexp, pattern: "foo", value: "foo", want: true},
|
||||
{name: "regexp_no_match", typ: MatchRegexp, pattern: "foo", value: "foobar", want: false},
|
||||
{name: "regexp_alternation", typ: MatchRegexp, pattern: "foo|bar", value: "bar", want: true},
|
||||
{name: "regexp_alternation_no_match", typ: MatchRegexp, pattern: "foo|bar", value: "baz", want: false},
|
||||
{name: "regexp_wildcard", typ: MatchRegexp, pattern: "fo.*", value: "foobar", want: true},
|
||||
{name: "regexp_prefix_anchored", typ: MatchRegexp, pattern: "fo", value: "foo", want: false},
|
||||
{name: "regexp_dot_plus", typ: MatchRegexp, pattern: ".+", value: "anything", want: true},
|
||||
{name: "regexp_dot_plus_empty", typ: MatchRegexp, pattern: ".+", value: "", want: false},
|
||||
|
||||
// MatchNotRegexp
|
||||
{name: "not_regexp_match", typ: MatchNotRegexp, pattern: "foo", value: "bar", want: true},
|
||||
{name: "not_regexp_no_match", typ: MatchNotRegexp, pattern: "foo", value: "foo", want: false},
|
||||
{name: "not_regexp_alternation", typ: MatchNotRegexp, pattern: "foo|bar", value: "baz", want: true},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
m, err := NewMatcher(tc.typ, "__name__", tc.pattern)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, m.Matches(tc.value))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMatcherBadRegexp(t *testing.T) {
|
||||
_, err := NewMatcher(MatchRegexp, "__name__", "[invalid")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMustNewMatcherPanics(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
MustNewMatcher(MatchRegexp, "__name__", "[invalid")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromStrings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []Label
|
||||
}{
|
||||
{
|
||||
name: "single_pair",
|
||||
args: []string{"__name__", "temp"},
|
||||
want: []Label{{Name: "__name__", Value: "temp"}},
|
||||
},
|
||||
{
|
||||
name: "multiple_pairs_sorted",
|
||||
args: []string{"room", "office", "__name__", "temp"},
|
||||
want: []Label{{Name: "__name__", Value: "temp"}, {Name: "room", Value: "office"}},
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
args: []string{},
|
||||
want: []Label{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := FromStrings(tc.args...)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromStringsPanicsOnOdd(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
FromStrings("__name__")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMatchTypeString(t *testing.T) {
|
||||
assert.Equal(t, "=", MatchEqual.String())
|
||||
assert.Equal(t, "!=", MatchNotEqual.String())
|
||||
assert.Equal(t, "=~", MatchRegexp.String())
|
||||
assert.Equal(t, "!~", MatchNotRegexp.String())
|
||||
}
|
||||
Reference in New Issue
Block a user