feat(models): New APIs to create Tags from a list of key, value pairs

* New API to convert `models.Tags` to a slice of key, value pairs

```
BenchmarkNewTagsKeyValues/sorted/no_dupes/preallocate-8         	20000000	        63.0 ns/op	       0 B/op	       0 allocs/op
BenchmarkNewTagsKeyValues/sorted/no_dupes/allocate-8            	10000000	       124 ns/op	     144 B/op	       1 allocs/op
BenchmarkNewTagsKeyValues/sorted/dupes-8                        	10000000	       181 ns/op	     240 B/op	       1 allocs/op
BenchmarkNewTagsKeyValues/unsorted/no_dupes-8                   	10000000	       204 ns/op	     176 B/op	       2 allocs/op
BenchmarkNewTagsKeyValues/unsorted/dupes-8                      	 5000000	       308 ns/op	     272 B/op	       2 allocs/op
```
pull/15033/head
Stuart Carnie 2019-09-06 15:22:57 -07:00
parent 61c75ae434
commit 4da079410f
No known key found for this signature in database
GPG Key ID: 848D9C9718D78B4F
2 changed files with 224 additions and 0 deletions

View File

@ -55,6 +55,10 @@ var (
// ErrInvalidPoint is returned when a point cannot be parsed correctly.
ErrInvalidPoint = errors.New("point is invalid")
// ErrInvalidKevValuePairs is returned when the number of key, value pairs
// is odd, indicating a missing value.
ErrInvalidKevValuePairs = errors.New("key, value pairs is an odd length")
)
const (
@ -2034,6 +2038,63 @@ func NewTags(m map[string]string) Tags {
return a
}
// NewTagsKeyValues returns a new Tags from a list of key, value pairs,
// ensuring the returned result is correctly sorted. Duplicate keys are removed,
// however, it which duplicate that remains is undefined.
// NewTagsKeyValues will return ErrInvalidKevValuePairs if len(kvs) is not even.
// If the input is guaranteed to be even, the error can be safely ignored.
// If a has enough capacity, it will be reused.
func NewTagsKeyValues(a Tags, kv ...[]byte) (Tags, error) {
if len(kv)%2 == 1 {
return nil, ErrInvalidKevValuePairs
}
if len(kv) == 0 {
return nil, nil
}
l := len(kv) / 2
if cap(a) < l {
a = make(Tags, 0, l)
} else {
a = a[:0]
}
for i := 0; i < len(kv)-1; i += 2 {
a = append(a, NewTag(kv[i], kv[i+1]))
}
if !a.sorted() {
sort.Sort(a)
}
// remove duplicates
j := 0
for i := 0; i < len(a)-1; i++ {
if !bytes.Equal(a[i].Key, a[i+1].Key) {
if j != i {
// only copy if j has deviated from i, indicating duplicates
a[j] = a[i]
}
j++
}
}
a[j] = a[len(a)-1]
j++
return a[:j], nil
}
// NewTagsKeyValuesStrings is equivalent to NewTagsKeyValues, except that
// it will allocate new byte slices for each key, value pair.
func NewTagsKeyValuesStrings(a Tags, kvs ...string) (Tags, error) {
kv := make([][]byte, len(kvs))
for i := range kvs {
kv[i] = []byte(kvs[i])
}
return NewTagsKeyValues(a, kv...)
}
// Keys returns the list of keys for a tag set.
func (a Tags) Keys() []string {
if len(a) == 0 {
@ -2100,6 +2161,34 @@ func (a Tags) Clone() Tags {
return others
}
// KeyValues returns the Tags as a list of key, value pairs,
// maintaining the original order of a. v will be used if it has
// capacity.
func (a Tags) KeyValues(v [][]byte) [][]byte {
l := a.Len() * 2
if cap(v) < l {
v = make([][]byte, 0, l)
} else {
v = v[:l]
}
for i := range a {
v = append(v, a[i].Key, a[i].Value)
}
return v
}
// sorted returns true if a is sorted and is an optimization
// to avoid an allocation when calling sort.IsSorted, improving
// performance as much as 50%.
func (a Tags) sorted() bool {
for i := len(a) - 1; i > 0; i-- {
if string(a[i].Key) < string(a[i-1].Key) {
return false
}
}
return true
}
func (a Tags) Len() int { return len(a) }
func (a Tags) Less(i, j int) bool { return bytes.Compare(a[i].Key, a[j].Key) == -1 }
func (a Tags) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

View File

@ -12,6 +12,7 @@ import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/influxdb/models"
)
@ -2619,6 +2620,90 @@ func TestValidTagTokens(t *testing.T) {
}
}
func equalError(a, b error) bool {
return a == nil && b == nil || a != nil && b != nil && a.Error() == b.Error()
}
func TestNewTagsKeyValues(t *testing.T) {
t.Run("sorted", func(t *testing.T) {
t.Run("no dupes", func(t *testing.T) {
got, _ := models.NewTagsKeyValuesStrings(nil, "tag0", "v0", "tag1", "v1", "tag2", "v2")
exp := models.NewTags(map[string]string{
"tag0": "v0",
"tag1": "v1",
"tag2": "v2",
})
if !cmp.Equal(got, exp) {
t.Errorf("unxpected; -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("dupes", func(t *testing.T) {
got, _ := models.NewTagsKeyValuesStrings(nil, "tag0", "v0", "tag1", "v1", "tag1", "v1", "tag2", "v2", "tag2", "v2")
exp := models.NewTags(map[string]string{
"tag0": "v0",
"tag1": "v1",
"tag2": "v2",
})
if !cmp.Equal(got, exp) {
t.Errorf("unxpected; -got/+exp\n%s", cmp.Diff(got, exp))
}
})
})
t.Run("unsorted", func(t *testing.T) {
t.Run("no dupes", func(t *testing.T) {
got, _ := models.NewTagsKeyValuesStrings(nil, "tag2", "v2", "tag0", "v0", "tag1", "v1")
exp := models.NewTags(map[string]string{
"tag0": "v0",
"tag1": "v1",
"tag2": "v2",
})
if !cmp.Equal(got, exp) {
t.Errorf("unxpected; -got/+exp\n%s", cmp.Diff(got, exp))
}
})
t.Run("dupes", func(t *testing.T) {
got, _ := models.NewTagsKeyValuesStrings(nil, "tag2", "v2", "tag0", "v0", "tag1", "v1", "tag2", "v2", "tag0", "v0", "tag1", "v1")
exp := models.NewTags(map[string]string{
"tag0": "v0",
"tag1": "v1",
"tag2": "v2",
})
if !cmp.Equal(got, exp) {
t.Errorf("unxpected; -got/+exp\n%s", cmp.Diff(got, exp))
}
})
})
t.Run("odd number of keys", func(t *testing.T) {
got, err := models.NewTagsKeyValuesStrings(nil, "tag2", "v2", "tag0", "v0", "tag1")
if !cmp.Equal(got, models.Tags(nil)) {
t.Errorf("expected nil")
}
if !cmp.Equal(err, models.ErrInvalidKevValuePairs, cmp.Comparer(equalError)) {
t.Errorf("expected ErrInvalidKevValuePairs, got: %v", err)
}
})
}
func TestTags_KeyValues(t *testing.T) {
tags := models.NewTags(map[string]string{
"tag0": "v0",
"tag1": "v1",
"tag2": "v2",
})
got := tags.KeyValues(nil)
exp := [][]byte{[]byte("tag0"), []byte("v0"), []byte("tag1"), []byte("v1"), []byte("tag2"), []byte("v2")}
if !cmp.Equal(got, exp) {
t.Errorf("unexpected, -got/+exp\n%s", cmp.Diff(got, exp))
}
}
func BenchmarkEscapeStringField_Plain(b *testing.B) {
s := "nothing special"
for i := 0; i < b.N; i++ {
@ -2749,3 +2834,53 @@ func BenchmarkMakeKey(b *testing.B) {
})
}
}
func BenchmarkNewTagsKeyValues(b *testing.B) {
b.Run("sorted", func(b *testing.B) {
b.Run("no dupes", func(b *testing.B) {
kv := [][]byte{[]byte("tag0"), []byte("v0"), []byte("tag1"), []byte("v1"), []byte("tag2"), []byte("v2")}
b.Run("preallocate", func(b *testing.B) {
t := make(models.Tags, 3)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = models.NewTagsKeyValues(t, kv...)
}
})
b.Run("allocate", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = models.NewTagsKeyValues(nil, kv...)
}
})
})
b.Run("dupes", func(b *testing.B) {
kv := [][]byte{[]byte("tag0"), []byte("v0"), []byte("tag1"), []byte("v1"), []byte("tag1"), []byte("v1"), []byte("tag2"), []byte("v2"), []byte("tag2"), []byte("v2")}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = models.NewTagsKeyValues(nil, kv...)
}
})
})
b.Run("unsorted", func(b *testing.B) {
b.Run("no dupes", func(b *testing.B) {
kv := [][]byte{[]byte("tag1"), []byte("v1"), []byte("tag0"), []byte("v0"), []byte("tag2"), []byte("v2")}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = models.NewTagsKeyValues(nil, kv...)
}
})
b.Run("dupes", func(b *testing.B) {
kv := [][]byte{[]byte("tag1"), []byte("v1"), []byte("tag2"), []byte("v2"), []byte("tag0"), []byte("v0"), []byte("tag1"), []byte("v1"), []byte("tag2"), []byte("v2")}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = models.NewTagsKeyValues(nil, kv...)
}
})
})
}