diff --git a/.travis.yml b/.travis.yml index fa097a3..5ddc6f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ sudo: required services: - docker before_install: - - docker run -d -p 10101:10101 pilosa/pilosa:v0.3.2 + - docker run -d -p 10101:10101 pilosa/pilosa:v0.4.0 - go get github.com/mattn/goveralls addons: apt: diff --git a/README.md b/README.md index 98e8689..b6aa429 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Go Client for Pilosa - + GoDoc @@ -12,9 +12,12 @@ Go client for Pilosa high performance distributed bitmap index. ## Change Log -* **v0.3.2**: +* **v0.4.0** (2017-06-09): + * Supports Pilosa Server v0.4.0. * Updated the accepted values for index, frame names and labels to match with the Pilosa server. * `Union` query now accepts zero or more variadic arguments. `Intersect` and `Difference` queries now accept one or more variadic arguments. + * Added `inverse TopN` and `inverse Range` calls. + * Inverse enabled status of frames is not checked on the client side. * **v0.3.1** (2017-05-01): * Initial version @@ -173,13 +176,18 @@ Index: Frame: * `Bitmap(rowID uint64) *PQLBitmapQuery` +* `InverseBitmap(rowID uint64) *PQLBitmapQuery` * `SetBit(rowID uint64, columnID uint64) *PQLBaseQuery` * `SetBitTimestamp(rowID uint64, columnID uint64, timestamp time.Time) *PQLBaseQuery` * `ClearBit(rowID uint64, columnID uint64) *PQLBaseQuery` * `TopN(n uint64) *PQLBitmapQuery` * `BitmapTopN(n uint64, bitmap *PQLBitmapQuery) *PQLBitmapQuery` * `FilterFieldTopN(n uint64, bitmap *PQLBitmapQuery, field string, values ...interface{}) *PQLBitmapQuery` +* `InverseTopN(n uint64) *PQLBitmapQuery` +* `InverseBitmapTopN(n uint64, bitmap *PQLBitmapQuery) *PQLBitmapQuery` +* `InverseFilterFieldTopN(n uint64, bitmap *PQLBitmapQuery, field string, values ...interface{}) *PQLBitmapQuery` * `Range(rowID uint64, start time.Time, end time.Time) *PQLBitmapQuery` +* `InverseRange(rowID uint64, start time.Time, end time.Time) *PQLBitmapQuery` * `SetBitmapAttrs(rowID uint64, attrs map[string]interface{}) *PQLBaseQuery` ### Pilosa URI diff --git a/client_it_test.go b/client_it_test.go index e93034d..959cbee 100644 --- a/client_it_test.go +++ b/client_it_test.go @@ -1,4 +1,4 @@ -// +build integration + +build integration // Copyright 2017 Pilosa Corp. // @@ -43,6 +43,7 @@ import ( "reflect" "strconv" "testing" + "time" "github.com/golang/protobuf/proto" "github.com/pilosa/go-pilosa/internal" @@ -179,6 +180,67 @@ func TestSetRowAttrs(t *testing.T) { } } +func TestOrmCount(t *testing.T) { + client := getClient() + countFrame, err := index.Frame("count-test", nil) + if err != nil { + t.Fatal(err) + } + err = client.EnsureFrame(countFrame) + if err != nil { + t.Fatal(err) + } + qry := index.BatchQuery( + countFrame.SetBit(10, 20), + countFrame.SetBit(10, 21), + countFrame.SetBit(15, 25), + ) + client.Query(qry, nil) + response, err := client.Query(index.Count(countFrame.Bitmap(10)), nil) + if err != nil { + t.Fatal(err) + } + if response.Result().Count != 2 { + t.Fatalf("Count should be 2") + } +} + +func TestTopNReturns(t *testing.T) { + client := getClient() + frame, err := index.Frame("topn_test", nil) + if err != nil { + t.Fatal(err) + } + err = client.EnsureFrame(frame) + if err != nil { + t.Fatal(err) + } + qry := index.BatchQuery( + frame.SetBit(10, 5), + frame.SetBit(10, 10), + frame.SetBit(10, 15), + frame.SetBit(20, 5), + frame.SetBit(30, 5), + ) + client.Query(qry, nil) + time.Sleep(10 * time.Second) + response, err := client.Query(frame.TopN(2), nil) + if err != nil { + t.Fatal(err) + } + items := response.Result().CountItems + if len(items) != 2 { + t.Fatalf("There should be 2 count items") + } + item := items[0] + if item.ID != 10 { + t.Fatalf("Item[0] ID should be 10") + } + if item.Count != 3 { + t.Fatalf("Item[0] Count should be 3") + } +} + func TestCreateDeleteIndexFrame(t *testing.T) { client := getClient() index1, err := NewIndex("to-be-deleted", nil) diff --git a/orm.go b/orm.go index ef62ec7..68faadd 100644 --- a/orm.go +++ b/orm.go @@ -358,9 +358,6 @@ func (f *Frame) Bitmap(rowID uint64) *PQLBitmapQuery { // Bitmap retrieves the indices of all the set bits in a row or column based on whether the row label or column label is given in the query. // It also retrieves any attributes set on that row or column. func (f *Frame) InverseBitmap(columnID uint64) *PQLBaseQuery { - if !f.options.InverseEnabled { - return NewPQLBaseQuery("", f.index, ErrorInverseBitmapsNotEnabled) - } return NewPQLBaseQuery(fmt.Sprintf("Bitmap(%s=%d, frame='%s')", f.index.options.ColumnLabel, columnID, f.name), f.index, nil) } @@ -391,19 +388,44 @@ func (f *Frame) ClearBit(rowID uint64, columnID uint64) *PQLBaseQuery { // TopN creates a TopN query with the given item count. // Returns the id and count of the top n bitmaps (by count of bits) in the frame. func (f *Frame) TopN(n uint64) *PQLBitmapQuery { - return NewPQLBitmapQuery(fmt.Sprintf("TopN(frame='%s', n=%d)", f.name, n), f.index, nil) + return NewPQLBitmapQuery(fmt.Sprintf("TopN(frame='%s', n=%d, inverse=false)", f.name, n), f.index, nil) +} + +// InverseTopN creates a TopN query with the given item count. +// Returns the id and count of the top n bitmaps (by count of bits) in the frame. +// This variant sets inverse=true +func (f *Frame) InverseTopN(n uint64) *PQLBitmapQuery { + return NewPQLBitmapQuery(fmt.Sprintf("TopN(frame='%s', n=%d, inverse=true)", f.name, n), f.index, nil) } // BitmapTopN creates a TopN query with the given item count and bitmap. -// This variant supports customizing the bitmap query. +// This variant supports customizing the bitmap query and sets inverse=true. func (f *Frame) BitmapTopN(n uint64, bitmap *PQLBitmapQuery) *PQLBitmapQuery { - return NewPQLBitmapQuery(fmt.Sprintf("TopN(%s, frame='%s', n=%d)", + return NewPQLBitmapQuery(fmt.Sprintf("TopN(%s, frame='%s', n=%d, inverse=false)", + bitmap.serialize(), f.name, n), f.index, nil) +} + +// InverseBitmapTopN creates a TopN query with the given item count and bitmap. +// This variant supports customizing the bitmap query and sets inverse=true. +func (f *Frame) InverseBitmapTopN(n uint64, bitmap *PQLBitmapQuery) *PQLBitmapQuery { + return NewPQLBitmapQuery(fmt.Sprintf("TopN(%s, frame='%s', n=%d, inverse=true)", bitmap.serialize(), f.name, n), f.index, nil) } // FilterFieldTopN creates a TopN query with the given item count, bitmap, field and the filter for that field // The field and filters arguments work together to only return Bitmaps which have the attribute specified by field with one of the values specified in filters. func (f *Frame) FilterFieldTopN(n uint64, bitmap *PQLBitmapQuery, field string, values ...interface{}) *PQLBitmapQuery { + return f.filterFieldTopN(n, bitmap, false, field, values...) +} + +// InverseFilterFieldTopN creates a TopN query with the given item count, bitmap, field and the filter for that field +// The field and filters arguments work together to only return Bitmaps which have the attribute specified by field with one of the values specified in filters. +// This variant sets inverse=true. +func (f *Frame) InverseFilterFieldTopN(n uint64, bitmap *PQLBitmapQuery, field string, values ...interface{}) *PQLBitmapQuery { + return f.filterFieldTopN(n, bitmap, true, field, values...) +} + +func (f *Frame) filterFieldTopN(n uint64, bitmap *PQLBitmapQuery, inverse bool, field string, values ...interface{}) *PQLBitmapQuery { if err := validateLabel(field); err != nil { return NewPQLBitmapQuery("", f.index, err) } @@ -411,14 +433,26 @@ func (f *Frame) FilterFieldTopN(n uint64, bitmap *PQLBitmapQuery, field string, if err != nil { return NewPQLBitmapQuery("", f.index, err) } - return NewPQLBitmapQuery(fmt.Sprintf("TopN(%s, frame='%s', n=%d, field='%s', %s)", - bitmap.serialize(), f.name, n, field, string(b)), f.index, nil) + inverseStr := "true" + if !inverse { + inverseStr = "false" + } + return NewPQLBitmapQuery(fmt.Sprintf("TopN(%s, frame='%s', n=%d, inverse=%s, field='%s', %s)", + bitmap.serialize(), f.name, n, inverseStr, field, string(b)), f.index, nil) } // Range creates a Range query. // Similar to Bitmap, but only returns bits which were set with timestamps between the given start and end timestamps. func (f *Frame) Range(rowID uint64, start time.Time, end time.Time) *PQLBitmapQuery { - return NewPQLBitmapQuery(fmt.Sprintf("Range(%s=%d, frame='%s', start='%s', end='%s')", + return NewPQLBitmapQuery(fmt.Sprintf("Range(%s=%d, frame='%s', start='%s', end='%s', inverse=false)", + f.options.RowLabel, rowID, f.name, start.Format(timeFormat), end.Format(timeFormat)), f.index, nil) +} + +// InverseRange creates a Range query. +// Similar to Bitmap, but only returns bits which were set with timestamps between the given start and end timestamps. +// This variant sets inverse=true. +func (f *Frame) InverseRange(rowID uint64, start time.Time, end time.Time) *PQLBitmapQuery { + return NewPQLBitmapQuery(fmt.Sprintf("Range(%s=%d, frame='%s', start='%s', end='%s', inverse=true)", f.options.RowLabel, rowID, f.name, start.Format(timeFormat), end.Format(timeFormat)), f.index, nil) } diff --git a/orm_test.go b/orm_test.go index 5189faf..ef33894 100644 --- a/orm_test.go +++ b/orm_test.go @@ -172,14 +172,23 @@ func TestDifference(t *testing.T) { func TestTopN(t *testing.T) { comparePQL(t, - "TopN(frame='sample-frame', n=27)", + "TopN(frame='sample-frame', n=27, inverse=false)", sampleFrame.TopN(27)) comparePQL(t, - "TopN(Bitmap(project=3, frame='collaboration'), frame='sample-frame', n=10)", + "TopN(frame='sample-frame', n=27, inverse=true)", + sampleFrame.InverseTopN(27)) + comparePQL(t, + "TopN(Bitmap(project=3, frame='collaboration'), frame='sample-frame', n=10, inverse=false)", sampleFrame.BitmapTopN(10, collabFrame.Bitmap(3))) comparePQL(t, - "TopN(Bitmap(project=7, frame='collaboration'), frame='sample-frame', n=12, field='category', [80,81])", + "TopN(Bitmap(project=3, frame='collaboration'), frame='sample-frame', n=10, inverse=true)", + sampleFrame.InverseBitmapTopN(10, collabFrame.Bitmap(3))) + comparePQL(t, + "TopN(Bitmap(project=7, frame='collaboration'), frame='sample-frame', n=12, inverse=false, field='category', [80,81])", sampleFrame.FilterFieldTopN(12, collabFrame.Bitmap(7), "category", 80, 81)) + comparePQL(t, + "TopN(Bitmap(project=7, frame='collaboration'), frame='sample-frame', n=12, inverse=true, field='category', [80,81])", + sampleFrame.InverseFilterFieldTopN(12, collabFrame.Bitmap(7), "category", 80, 81)) } func TestFilterFieldTopNInvalidField(t *testing.T) { @@ -296,8 +305,11 @@ func TestRange(t *testing.T) { start := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2000, time.February, 2, 3, 4, 0, 0, time.UTC) comparePQL(t, - "Range(project=10, frame='collaboration', start='1970-01-01T00:00', end='2000-02-02T03:04')", + "Range(project=10, frame='collaboration', start='1970-01-01T00:00', end='2000-02-02T03:04', inverse=false)", collabFrame.Range(10, start, end)) + comparePQL(t, + "Range(project=10, frame='collaboration', start='1970-01-01T00:00', end='2000-02-02T03:04', inverse=true)", + collabFrame.InverseRange(10, start, end)) } func TestInvalidColumnLabelFails(t *testing.T) { @@ -319,17 +331,6 @@ func TestInvalidRowLabelFails(t *testing.T) { } } -func TestInverseBitmapFailsIfNotEnabled(t *testing.T) { - frame, err := sampleIndex.Frame("inverse-not-enabled", nil) - if err != nil { - t.Fatal(err) - } - qry := frame.InverseBitmap(5) - if qry.Error() == nil { - t.Fatalf("Creating InverseBitmap query for a frame without inverse frame enabled should fail") - } -} - func TestFrameOptionsToString(t *testing.T) { frameOptions := &FrameOptions{ RowLabel: "stargazer_id",