diff --git a/index.go b/index.go index cdaade4c05..8a813d95de 100644 --- a/index.go +++ b/index.go @@ -235,6 +235,75 @@ func (t *Index) TagKeys(names []string) []string { return sortedKeys } +// TagValues returns a map of unique tag values for the given measurements and key with the given filters applied. +// Call .ToSlice() on the result to convert it into a sorted slice of strings. +// Filters are equivalent to and AND operation. If you want to do an OR, get the tag values for one set, +// then get the tag values for another set and do a union of the two. +func (t *Index) TagValues(names []string, key string, filters []*Filter) TagValues { + t.mu.RLock() + defer t.mu.RUnlock() + + values := TagValues(make(map[string]bool)) + + // see if they just want all the tag values for this key + if len(filters) == 0 { + for _, n := range names { + idx := t.measurements[n] + if idx != nil { + values.Union(idx.tagValues(key)) + } + } + return values + } + + // they have filters so just get a set of series ids matching them and then get the tag values from those + seriesIDs := t.SeriesIDs(names, filters) + return t.tagValuesForSeries(key, seriesIDs) +} + +// tagValuesForSeries will return a TagValues map of all the unique tag values for a collection of series. +func (t *Index) tagValuesForSeries(key string, seriesIDs SeriesIDs) TagValues { + values := make(map[string]bool) + for _, id := range seriesIDs { + s := t.series[id] + if s == nil { + continue + } + if v, ok := s.Tags[key]; ok { + values[v] = true + } + } + return TagValues(values) +} + +type TagValues map[string]bool + +// ToSlice returns a sorted slice of the TagValues +func (t TagValues) ToSlice() []string { + a := make([]string, 0, len(t)) + for v, _ := range t { + a = append(a, v) + } + sort.Strings(a) + return a +} + +// Union will modify the receiver by merging in the passed in values. +func (l TagValues) Union(r TagValues) { + for v, _ := range r { + l[v] = true + } +} + +// Intersect will modify the receiver by keeping only the keys that exist in the passed in values +func (l TagValues) Intersect(r TagValues) { + for v, _ := range l { + if _, ok := r[v]; !ok { + delete(l, v) + } + } +} + //seriesIDsForName is the same as SeriesIDs, but for a specific measurement. func (t *Index) seriesIDsForName(name string, filters Filters) SeriesIDs { idx := t.measurements[name] diff --git a/index_test.go b/index_test.go index 2d16e770e3..9991087437 100644 --- a/index_test.go +++ b/index_test.go @@ -338,7 +338,81 @@ func TestIndex_TagKeys(t *testing.T) { } func TestIndex_TagValuesWhereFilter(t *testing.T) { - t.Skip("pending") + idx := indexWithFixtureData() + + var tests = []struct { + names []string + key string + filters []*influxdb.Filter + result []string + }{ + // get the tag values across multiple measurements + + // get the tag values for a single measurement + { + names: []string{"key_count"}, + key: "region", + result: []string{"useast", "uswest"}, + }, + + // get the tag values for a single measurement with where filter + { + names: []string{"key_count"}, + key: "region", + filters: []*influxdb.Filter{ + &influxdb.Filter{Key: "host", Value: "serverc.influx.com"}, + }, + result: []string{"uswest"}, + }, + + // get the tag values for a single measurement with a not where filter + { + names: []string{"key_count"}, + key: "region", + filters: []*influxdb.Filter{ + &influxdb.Filter{Key: "host", Value: "serverc.influx.com", Not: true}, + }, + result: []string{"useast"}, + }, + + // get the tag values for a single measurement with multiple where filters + { + names: []string{"key_count"}, + key: "region", + filters: []*influxdb.Filter{ + &influxdb.Filter{Key: "host", Value: "serverc.influx.com"}, + &influxdb.Filter{Key: "service", Value: "redis"}, + }, + result: []string{"uswest"}, + }, + + // get the tag values for a single measurement with regex filter + { + names: []string{"queue_depth"}, + key: "name", + filters: []*influxdb.Filter{ + &influxdb.Filter{Key: "app", Regex: regexp.MustCompile("paul.*")}, + }, + result: []string{"high priority"}, + }, + + // get the tag values for a single measurement with a not regex filter + { + names: []string{"key_count"}, + key: "region", + filters: []*influxdb.Filter{ + &influxdb.Filter{Key: "host", Regex: regexp.MustCompile("serverd.*"), Not: true}, + }, + result: []string{"uswest"}, + }, + } + + for i, tt := range tests { + r := idx.TagValues(tt.names, tt.key, tt.filters).ToSlice() + if !reflect.DeepEqual(r, tt.result) { + t.Fatalf("%d: filters: %s: result mismatch:\n exp=%s\n got=%s", i, mustMarshalJSON(tt.filters), tt.result, r) + } + } } func TestIndex_TagValuesWhereFilterMultiple(t *testing.T) { diff --git a/server.go b/server.go index 4f6f85057b..7198baa3c5 100644 --- a/server.go +++ b/server.go @@ -1264,13 +1264,17 @@ type databaseJSON struct { Shards []*Shard `json:"shards,omitempty"` } -// Measurement represents a collection of time series in a database +// Measurement represents a collection of time series in a database. It also contains in memory +// structures for indexing tags. These structures are accessed through private methods on the Measurement +// object. Generally these methods are only accessed from Index, which is responsible for ensuring +// go routine safe access. type Measurement struct { Name string `json:"name,omitempty"` Fields []*Fields `json:"fields,omitempty"` // in memory index fields series map[string]*Series // sorted tagset string to the series object + seriesByID map[uint32]*Series // lookup table for series by their id measurement *Measurement seriesByTagKeyValue map[string]map[string]SeriesIDs // map from tag key to value to sorted set of series ids ids SeriesIDs // sorted list of series IDs in this measurement @@ -1282,6 +1286,7 @@ func NewMeasurement(name string) *Measurement { Fields: make([]*Fields, 0), series: make(map[string]*Series), + seriesByID: make(map[uint32]*Series), seriesByTagKeyValue: make(map[string]map[string]SeriesIDs), ids: SeriesIDs(make([]uint32, 0)), } @@ -1289,10 +1294,11 @@ func NewMeasurement(name string) *Measurement { // addSeries will add a series to the measurementIndex. Returns false if already present func (m *Measurement) addSeries(s *Series) bool { - tagset := string(marshalTags(s.Tags)) - if _, ok := m.series[tagset]; ok { + if _, ok := m.seriesByID[s.ID]; ok { return false } + m.seriesByID[s.ID] = s + tagset := string(marshalTags(s.Tags)) m.series[tagset] = s m.ids = append(m.ids, s.ID) // the series ID should always be higher than all others because it's a new @@ -1374,9 +1380,17 @@ func (m *Measurement) seriesIDs(filter *Filter) (ids SeriesIDs) { return } -type Measurements []*Measurement +// tagValues returns an array of unique tag values for the given key +func (m *Measurement) tagValues(key string) TagValues { + tags := m.seriesByTagKeyValue[key] + values := make(map[string]bool, len(tags)) + for k, _ := range tags { + values[k] = true + } + return TagValues(values) +} -func (m Measurement) String() string { return string(mustMarshalJSON(m)) } +type Measurements []*Measurement // Field represents a series field. type Field struct {