Add an IncludesColumn() function to PQL

Usage:
`IncludesColumn(Intersect(Row(a=1), Row(b=2)), column=10)`

The above query will return a `bool` indicating whether the
intersection of rows a-1 and b-2 contains column 10. Because
a single column is specified, this executes on a single shard
(shard=0 in this example).
This commit is contained in:
Travis 2019-11-09 16:39:01 -06:00
parent 198626e657
commit ed37ef5dcf
4 changed files with 140 additions and 1 deletions

View file

@ -341,6 +341,8 @@ func (e *executor) executeCall(ctx context.Context, index string, c *pql.Call, s
return e.executeGroupBy(ctx, index, c, shards, opt)
case "Options":
return e.executeOptionsCall(ctx, index, c, shards, opt)
case "IncludesColumn":
return e.executeIncludesColumnCall(ctx, index, c, shards, opt)
default:
e.Holder.Stats.CountWithCustomTags(c.Name, 1, 1.0, []string{indexTag})
return e.executeBitmapCall(ctx, index, c, shards, opt)
@ -411,6 +413,58 @@ func (e *executor) executeOptionsCall(ctx context.Context, index string, c *pql.
return e.executeCall(ctx, index, c.Children[0], shards, optCopy)
}
// executeIncludesColumnCall executes an IncludesColumn() call.
func (e *executor) executeIncludesColumnCall(ctx context.Context, index string, c *pql.Call, shards []uint64, opt *execOptions) (bool, error) {
// Get the shard containing the column, since that's the only
// shard that needs to execute this query.
var shard uint64
col, ok, err := c.UintArg("column")
if err != nil {
return false, errors.Wrap(err, "getting column from args")
} else if !ok {
return false, errors.New("IncludesColumn call must specify a column")
}
shard = col / ShardWidth
// If shard is not in shards, bail early.
if !uint64InSlice(shard, shards) {
return false, nil
}
// Execute calls in bulk on each remote node and merge.
mapFn := func(shard uint64) (interface{}, error) {
return e.executeIncludesColumnCallShard(ctx, index, c, shard, col)
}
// Merge returned results at coordinating node.
reduceFn := func(prev, v interface{}) interface{} {
other, _ := prev.(bool)
return other || v.(bool)
}
result, err := e.mapReduce(ctx, index, []uint64{shard}, c, opt, mapFn, reduceFn)
if err != nil {
return false, err
}
return result.(bool), nil
}
// executeIncludesColumnCallShard
func (e *executor) executeIncludesColumnCallShard(ctx context.Context, index string, c *pql.Call, shard uint64, column uint64) (bool, error) {
span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeIncludesColumnCallShard")
defer span.Finish()
if len(c.Children) == 1 {
row, err := e.executeBitmapCallShard(ctx, index, c.Children[0], shard)
if err != nil {
return false, errors.Wrap(err, "executing bitmap call")
}
return row.Includes(column), nil
}
return false, errors.New("IncludesColumn call must specify a row query")
}
// executeSum executes a Sum() call.
func (e *executor) executeSum(ctx context.Context, index string, c *pql.Call, shards []uint64, opt *execOptions) (ValCount, error) {
span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeSum")

View file

@ -4090,3 +4090,60 @@ func TestExecutor_Execute_Shift(t *testing.T) {
}
})
}
func TestExecutor_Execute_IncludesColumn(t *testing.T) {
t.Run("results", func(t *testing.T) {
c := test.MustRunCluster(t, 1)
defer c.Close()
hldr := test.Holder{Holder: c[0].Server.Holder()}
hldr.SetBit("i", "general", 10, 1)
hldr.SetBit("i", "general", 10, ShardWidth)
hldr.SetBit("i", "general", 10, 2*ShardWidth)
for i, tt := range []struct {
col uint64
expIncluded bool
}{
{1, true},
{2, false},
{ShardWidth, true},
{ShardWidth + 1, false},
{2 * ShardWidth, true},
{(2 * ShardWidth) + 1, false},
} {
t.Run(fmt.Sprint(i), func(t *testing.T) {
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: fmt.Sprintf("IncludesColumn(Row(general=10), column=%d)", tt.col)}); err != nil {
t.Fatal(err)
} else if tt.expIncluded && !res.Results[0].(bool) {
t.Fatalf("expected to find column: %d", tt.col)
} else if !tt.expIncluded && res.Results[0].(bool) {
t.Fatalf("did not expect to find column: %d", tt.col)
}
})
}
})
t.Run("errors", func(t *testing.T) {
c := test.MustRunCluster(t, 1)
defer c.Close()
hldr := test.Holder{Holder: c[0].Server.Holder()}
hldr.SetBit("i", "general", 10, 1)
t.Run("no column", func(t *testing.T) {
expErr := "IncludesColumn call must specify a column"
if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `IncludesColumn(Row(general=10))`}); err == nil {
t.Fatalf("expected to get an error")
} else if !strings.Contains(err.Error(), expErr) {
t.Fatalf("expected error: %s, but got: %s", expErr, err.Error())
}
})
t.Run("no row query", func(t *testing.T) {
expErr := "IncludesColumn call must specify a row query"
if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `IncludesColumn(column=1)`}); err == nil {
t.Fatalf("expected to get an error")
} else if !strings.Contains(err.Error(), expErr) {
t.Fatalf("expected error: %s, but got: %s", expErr, err.Error())
}
})
})
}

15
row.go
View file

@ -326,6 +326,21 @@ func (r *Row) Columns() []uint64 {
return a
}
// Includes returns true if the row contains the given column.
func (r *Row) Includes(col uint64) bool {
// TODO: improve the efficiency of this method by
// performing the column filter at the bitmap level
// rather than iterating through the results here.
for i := range r.segments {
for _, c := range r.segments[i].Columns() {
if c == col {
return true
}
}
}
return false
}
// rowSegment holds a subset of a row.
// This could point to a mmapped roaring bitmap or an in-memory bitmap. The
// width of the segment will always match the shard width.

View file

@ -123,5 +123,18 @@ func TestRow_IsEmpty(t *testing.T) {
if !res.IsEmpty() {
t.Fatal("Result Should Be Empty\n")
}
}
func TestRow_Includes(t *testing.T) {
row := pilosa.NewRow(0, 2*ShardWidth)
if !row.Includes(0) {
t.Fatal("row should include 0")
}
if row.Includes(1) {
t.Fatal("row should not include 1")
}
if !row.Includes(2 * ShardWidth) {
t.Fatalf("row should include %d", 2*ShardWidth)
}
}