mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-06 16:45:55 +00:00
implement ClearRow() query
This commit is contained in:
parent
365fbdf244
commit
b6e99734f0
7 changed files with 1643 additions and 1220 deletions
|
|
@ -236,6 +236,36 @@ Clear(10, stargazer=1)
|
|||
|
||||
This represents removing the relationship between the user with id=1 and the repository with id=10.
|
||||
|
||||
#### ClearRow
|
||||
|
||||
**Spec:**
|
||||
|
||||
```
|
||||
ClearRow(<FIELD>=<ROW>)
|
||||
```
|
||||
|
||||
**Description:**
|
||||
|
||||
`ClearRow` sets all bits to 0 in a given row of the binary matrix, thus disassociating the given row in the given field from all columns.
|
||||
|
||||
**Result Type:** boolean
|
||||
|
||||
A return value of `true` indicates that at least one column was toggled from 1 to 0.
|
||||
|
||||
A return value of `false` indicates that all bits in the row were already 0 and nothing changed.
|
||||
|
||||
**Examples:**
|
||||
|
||||
Clear all bit in row 1 in the stargazer field:
|
||||
```request
|
||||
ClearRow(stargazer=1)
|
||||
```
|
||||
```response
|
||||
{"results":[true]}
|
||||
```
|
||||
|
||||
This represents removing the relationship between the user with id=1 and all repositories.
|
||||
|
||||
### Read Operations
|
||||
|
||||
#### Row
|
||||
|
|
|
|||
74
executor.go
74
executor.go
|
|
@ -182,6 +182,8 @@ func (e *executor) executeCall(ctx context.Context, index string, c *pql.Call, s
|
|||
return e.executeMax(ctx, index, c, shards, opt)
|
||||
case "Clear":
|
||||
return e.executeClearBit(ctx, index, c, opt)
|
||||
case "ClearRow":
|
||||
return e.executeClearRow(ctx, index, c, shards, opt)
|
||||
case "Count":
|
||||
e.Holder.Stats.CountWithCustomTags(c.Name, 1, 1.0, []string{indexTag})
|
||||
return e.executeCount(ctx, index, c, shards, opt)
|
||||
|
|
@ -1107,7 +1109,7 @@ func (e *executor) executeClearBit(ctx context.Context, index string, c *pql.Cal
|
|||
return e.executeClearBitField(ctx, index, c, f, colID, rowID, opt)
|
||||
}
|
||||
|
||||
// executeClearBitField executes a Clear() call for a single view.
|
||||
// executeClearBitField executes a Clear() call for a field.
|
||||
func (e *executor) executeClearBitField(ctx context.Context, index string, c *pql.Call, f *Field, colID, rowID uint64, opt *execOptions) (bool, error) {
|
||||
shard := colID / ShardWidth
|
||||
ret := false
|
||||
|
|
@ -1137,6 +1139,76 @@ func (e *executor) executeClearBitField(ctx context.Context, index string, c *pq
|
|||
return ret, nil
|
||||
}
|
||||
|
||||
// executeClearRow executes a ClearRow() call.
|
||||
func (e *executor) executeClearRow(ctx context.Context, index string, c *pql.Call, shards []uint64, opt *execOptions) (bool, error) {
|
||||
// Ensure the field type supports ClearRow().
|
||||
fieldName, err := c.FieldArg()
|
||||
if err != nil {
|
||||
return false, errors.New("ClearRow() argument required: field")
|
||||
}
|
||||
field := e.Holder.Field(index, fieldName)
|
||||
if field == nil {
|
||||
return false, ErrFieldNotFound
|
||||
}
|
||||
if field.Type() == FieldTypeInt {
|
||||
return false, errors.New("ClearRow() is not supported on `int` fields")
|
||||
}
|
||||
|
||||
// Execute calls in bulk on each remote node and merge.
|
||||
mapFn := func(shard uint64) (interface{}, error) {
|
||||
return e.executeClearRowShard(ctx, index, c, shard)
|
||||
}
|
||||
|
||||
// Merge returned results at coordinating node.
|
||||
reduceFn := func(prev, v interface{}) interface{} {
|
||||
val := v.(bool)
|
||||
if prev == nil {
|
||||
return val
|
||||
}
|
||||
return val || prev.(bool)
|
||||
}
|
||||
|
||||
result, err := e.mapReduce(ctx, index, shards, c, opt, mapFn, reduceFn)
|
||||
return result.(bool), err
|
||||
}
|
||||
|
||||
// executeClearRowShard executes a ClearRow() call for a single shard.
|
||||
func (e *executor) executeClearRowShard(_ context.Context, index string, c *pql.Call, shard uint64) (bool, error) {
|
||||
fieldName, err := c.FieldArg()
|
||||
if err != nil {
|
||||
return false, errors.New("ClearRow() argument required: field")
|
||||
}
|
||||
|
||||
// Read fields using labels.
|
||||
rowID, ok, err := c.UintArg(fieldName)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("reading ClearRow() row: %v", err)
|
||||
} else if !ok {
|
||||
return false, fmt.Errorf("ClearRow() row argument '%v' required", rowLabel)
|
||||
}
|
||||
|
||||
field := e.Holder.Field(index, fieldName)
|
||||
if field == nil {
|
||||
return false, ErrFieldNotFound
|
||||
}
|
||||
|
||||
// Remove the row from all views.
|
||||
changed := false
|
||||
for _, view := range field.views() {
|
||||
fragment := e.Holder.fragment(index, fieldName, view.name, shard)
|
||||
if fragment == nil {
|
||||
continue
|
||||
}
|
||||
cleared, err := fragment.clearRow(rowID)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "clearing row %d on view %s shard %d", rowID, view.name, shard)
|
||||
}
|
||||
changed = changed || cleared
|
||||
}
|
||||
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// executeSet executes a Set() call.
|
||||
func (e *executor) executeSet(ctx context.Context, index string, c *pql.Call, opt *execOptions) (bool, error) {
|
||||
// Read colID.
|
||||
|
|
|
|||
186
executor_test.go
186
executor_test.go
|
|
@ -1657,6 +1657,192 @@ func TestExecutor_Execute_Not(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure a row can be cleared.
|
||||
func TestExecutor_Execute_ClearRow(t *testing.T) {
|
||||
t.Run("Set", func(t *testing.T) {
|
||||
c := test.MustRunCluster(t, 1)
|
||||
defer c.Close()
|
||||
hldr := test.Holder{Holder: c[0].Server.Holder()}
|
||||
index := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{TrackExistence: true})
|
||||
_, err := index.CreateField("f", pilosa.OptFieldTypeDefault())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set bits.
|
||||
if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `` +
|
||||
fmt.Sprintf("Set(%d, f=%d)\n", 3, 10) +
|
||||
fmt.Sprintf("Set(%d, f=%d)\n", ShardWidth-1, 10) +
|
||||
fmt.Sprintf("Set(%d, f=%d)\n", ShardWidth+1, 10) +
|
||||
fmt.Sprintf("Set(%d, f=%d)\n", 1, 20) +
|
||||
fmt.Sprintf("Set(%d, f=%d)\n", ShardWidth+1, 20),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Row(f=10)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{3, ShardWidth - 1, ShardWidth + 1}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
}
|
||||
|
||||
// Clear the row and ensure we get a `true` response.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `ClearRow(f=10)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if res := res.Results[0].(bool); !res {
|
||||
t.Fatalf("unexpected clear row result: %+v", res)
|
||||
}
|
||||
|
||||
// Clear the row again and ensure we get a `false` response.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `ClearRow(f=10)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if res := res.Results[0].(bool); res {
|
||||
t.Fatalf("unexpected clear row result: %+v", res)
|
||||
}
|
||||
|
||||
// Ensure the row is empty.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Row(f=10)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
}
|
||||
|
||||
// Ensure other rows were not affected.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Row(f=20)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{1, ShardWidth + 1}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
}
|
||||
})
|
||||
t.Run("Mutex", func(t *testing.T) {
|
||||
c := test.MustRunCluster(t, 1)
|
||||
defer c.Close()
|
||||
hldr := test.Holder{Holder: c[0].Server.Holder()}
|
||||
index := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{TrackExistence: true})
|
||||
_, err := index.CreateField("f", pilosa.OptFieldTypeMutex("none", 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set bits.
|
||||
if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `` +
|
||||
fmt.Sprintf("Set(%d, f=%d)\n", 3, 10) +
|
||||
fmt.Sprintf("Set(%d, f=%d)\n", ShardWidth-1, 10) +
|
||||
fmt.Sprintf("Set(%d, f=%d)\n", ShardWidth+1, 10) +
|
||||
fmt.Sprintf("Set(%d, f=%d)\n", 1, 20) +
|
||||
fmt.Sprintf("Set(%d, f=%d)\n", ShardWidth+1, 20),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Row(f=10)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{3, ShardWidth - 1}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
}
|
||||
|
||||
// Clear the row and ensure we get a `true` response.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `ClearRow(f=10)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if res := res.Results[0].(bool); !res {
|
||||
t.Fatalf("unexpected clear row result: %+v", res)
|
||||
}
|
||||
|
||||
// Clear the row again and ensure we get a `false` response.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `ClearRow(f=10)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if res := res.Results[0].(bool); res {
|
||||
t.Fatalf("unexpected clear row result: %+v", res)
|
||||
}
|
||||
|
||||
// Ensure the row is empty.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Row(f=10)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
}
|
||||
|
||||
// Ensure other rows were not affected.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Row(f=20)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{1, ShardWidth + 1}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
}
|
||||
})
|
||||
t.Run("Time", func(t *testing.T) {
|
||||
c := test.MustRunCluster(t, 1)
|
||||
defer c.Close()
|
||||
hldr := test.Holder{Holder: c[0].Server.Holder()}
|
||||
index := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{TrackExistence: true})
|
||||
_, err := index.CreateField("f", pilosa.OptFieldTypeTime(pilosa.TimeQuantum("YMD")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set columns.
|
||||
cc := `
|
||||
Set(2, f=1, 1999-12-31T00:00)
|
||||
Set(3, f=1, 2000-01-01T00:00)
|
||||
Set(4, f=1, 2000-01-02T00:00)
|
||||
Set(5, f=1, 2000-02-01T00:00)
|
||||
Set(6, f=1, 2001-01-01T00:00)
|
||||
Set(7, f=1, 2002-01-01T02:00)
|
||||
|
||||
Set(2, f=1, 1999-12-30T00:00)
|
||||
Set(2, f=1, 2002-02-01T00:00)
|
||||
Set(2, f=10, 2001-01-01T00:00)
|
||||
`
|
||||
rangeCheckQuery1 := `Range(f=1, 1999-12-31T00:00, 2003-01-01T03:00)`
|
||||
rangeCheckQuery10 := `Range(f=10, 1999-12-31T00:00, 2003-01-01T03:00)`
|
||||
|
||||
if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: cc}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: rangeCheckQuery1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if columns := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(columns, []uint64{2, 3, 4, 5, 6, 7}) {
|
||||
t.Fatalf("unexpected columns: %+v", columns)
|
||||
}
|
||||
|
||||
// Clear the row and ensure we get a `true` response.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `ClearRow(f=1)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if res := res.Results[0].(bool); !res {
|
||||
t.Fatalf("unexpected clear row result: %+v", res)
|
||||
}
|
||||
|
||||
// Ensure the row is empty.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: rangeCheckQuery1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if columns := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(columns, []uint64{}) {
|
||||
t.Fatalf("unexpected columns: %+v", columns)
|
||||
}
|
||||
|
||||
// Ensure other rows were not affected.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: rangeCheckQuery10}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if columns := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(columns, []uint64{2}) {
|
||||
t.Fatalf("unexpected columns: %+v", columns)
|
||||
}
|
||||
})
|
||||
t.Run("Int", func(t *testing.T) {
|
||||
c := test.MustRunCluster(t, 1)
|
||||
defer c.Close()
|
||||
hldr := test.Holder{Holder: c[0].Server.Holder()}
|
||||
index := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{TrackExistence: true})
|
||||
_, err := index.CreateField("f", pilosa.OptFieldTypeInt(0, 100))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ensure that clearing a row raises an error.
|
||||
if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `ClearRow(f=1)`}); err == nil {
|
||||
t.Fatal("expected clear row to return an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func benchmarkExistence(nn bool, b *testing.B) {
|
||||
c := test.MustRunCluster(b, 1)
|
||||
defer c.Close()
|
||||
|
|
|
|||
45
fragment.go
45
fragment.go
|
|
@ -490,6 +490,45 @@ func (f *fragment) unprotectedClearBit(rowID, columnID uint64) (changed bool, er
|
|||
return changed, nil
|
||||
}
|
||||
|
||||
// ClearRow clears a row for a given rowID within the fragment.
|
||||
// This updates both the on-disk storage and the in-cache bitmap.
|
||||
func (f *fragment) clearRow(rowID uint64) (bool, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.unprotectedClearRow(rowID)
|
||||
}
|
||||
|
||||
func (f *fragment) unprotectedClearRow(rowID uint64) (changed bool, err error) {
|
||||
changed = false
|
||||
|
||||
// First container of the row in storage.
|
||||
headContainerKey := rowID << shardVsContainerExponent
|
||||
|
||||
// Remove every container in the row.
|
||||
for i := uint64(0); i < (1 << shardVsContainerExponent); i++ {
|
||||
k := headContainerKey + i
|
||||
// Technically we could bypass the Get() call and only
|
||||
// call Remove(), but the Get() gives us the ability
|
||||
// to return true if any existing data was removed.
|
||||
if cont := f.storage.Containers.Get(k); cont != nil {
|
||||
f.storage.Containers.Remove(k)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the row in cache.
|
||||
f.cache.Add(rowID, 0)
|
||||
|
||||
// Snapshot storage.
|
||||
if err := f.snapshot(); err != nil {
|
||||
return false, errors.Wrap(err, "snapshotting")
|
||||
}
|
||||
|
||||
f.stats.Count("clearRow", 1, 1.0)
|
||||
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
func (f *fragment) bit(rowID, columnID uint64) (bool, error) {
|
||||
pos, err := f.pos(rowID, columnID)
|
||||
if err != nil {
|
||||
|
|
@ -2182,6 +2221,12 @@ func pos(rowID, columnID uint64) uint64 {
|
|||
type vector interface {
|
||||
Get(colID uint64) (uint64, bool)
|
||||
Set(colID, rowID uint64)
|
||||
// TODO: Clear(colID, rowID uint64)
|
||||
// Set() and Clear() are required for implementations
|
||||
// where the vector data is maintained independently
|
||||
// from the fragment data. In those cases, the Set()
|
||||
// and Clear() would also need to address the ranked
|
||||
// cache as well as the effects of ClearRow().
|
||||
}
|
||||
|
||||
// rowsVector implements the vector interface by looking
|
||||
|
|
|
|||
|
|
@ -95,6 +95,33 @@ func TestFragment_ClearBit(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure a fragment can clear a row.
|
||||
func TestFragment_ClearRow(t *testing.T) {
|
||||
f := mustOpenFragment("i", "f", viewStandard, 0, "")
|
||||
defer f.Close()
|
||||
|
||||
// Set and then clear bits on the fragment.
|
||||
if _, err := f.setBit(1000, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := f.setBit(1000, 65536); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := f.unprotectedClearRow(1000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify count on row.
|
||||
if n := f.row(1000).Count(); n != 0 {
|
||||
t.Fatalf("unexpected count: %d", n)
|
||||
}
|
||||
|
||||
// Close and reopen the fragment & verify the data.
|
||||
if err := f.reopen(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if n := f.row(1000).Count(); n != 0 {
|
||||
t.Fatalf("unexpected count (reopen): %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure a fragment can set & read a value.
|
||||
func TestFragment_SetValue(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ Call <- 'Set' {p.startCall("Set")} open col comma args (comma timestamp)? close
|
|||
/ 'SetRowAttrs' {p.startCall("SetRowAttrs")} open posfield comma row comma args close {p.endCall()}
|
||||
/ 'SetColumnAttrs' {p.startCall("SetColumnAttrs")} open col comma args close {p.endCall()}
|
||||
/ 'Clear' {p.startCall("Clear")} open col comma args close {p.endCall()}
|
||||
/ 'ClearRow' {p.startCall("ClearRow")} open arg close {p.endCall()}
|
||||
/ 'TopN' {p.startCall("TopN")} open posfield (comma allargs)? close {p.endCall()}
|
||||
/ 'Range' {p.startCall("Range")} open (timerange / conditional / arg) close {p.endCall()}
|
||||
/ < IDENT > { p.startCall(buffer[begin:end] ) } open allargs comma? close { p.endCall() }
|
||||
|
|
|
|||
2500
pql/pql.peg.go
2500
pql/pql.peg.go
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue