featurebase/roaring/filter.go
reesporte 48aef0c8a4 add copyright notice back in
```bash
for file in `cat diffys`; do
   printf '%s\n%s\n' "// Copyright 2021 Molecula Corp. All rights reserved." "$(cat $file)" >$file;
done
```
2021-12-10 11:01:04 -06:00

879 lines
28 KiB
Go

// Copyright 2021 Molecula Corp. All rights reserved.
package roaring
import (
"errors"
"fmt"
"github.com/molecula/featurebase/v2/shardwidth"
)
// We want BitmapScanner to be accessible from both the pilosa package, and
// the rbf package. Pilosa imports rbf, so rbf can't import pilosa, but they
// both import roaring, and this package is closely tied to roaring structures
// like Containers and the key/container mapping, so it mostly makes sense for
// this to be here.
//
// Unfortunately, this really needs to be capable of being row-aware, which
// means it needs access to the shard width stuff, which roaring otherwise
// studiously avoids knowing about.
const (
rowExponent = (shardwidth.Exponent - 16) // for instance, 20-16 = 4
rowWidth = 1 << rowExponent // containers per row, for instance 1<<4 = 16
keyMask = (rowWidth - 1) // a mask for offset within the row
rowMask = ^FilterKey(keyMask) // a mask for the row bits, without converting them to a row ID
)
type FilterKey uint64
// FilterResult represents the results of a BitmapFilter considering a
// key, or data. The values are represented as exclusive upper bounds on
// a series of matches followed by a series of rejections. So for instance,
// if called on key 23, the result {YesKey: 23, NoKey: 24} indicates that
// key 23 is a "no". This may seem confusing but it makes the math a lot
// easier to write. It can also report an error, which indicates that the
// entire operation should be stopped with that error.
type FilterResult struct {
YesKey FilterKey // The lowest container key this filter is known NOT to match.
NoKey FilterKey // The highest container key after YesKey that this filter is known to not match.
Err error // An error which should terminate processing.
}
// Row() computes the row number of a key.
func (f FilterKey) Row() uint64 {
return uint64(f >> rowExponent)
}
// Add adds an offset to a key.
func (f FilterKey) Add(x uint64) FilterKey {
return f + FilterKey(x)
}
// Sub determines the distance from o to f.
func (f FilterKey) Sub(o FilterKey) uint64 {
return uint64(f - o)
}
// MatchReject just sets Yes and No appropriately.
func (f FilterKey) MatchReject(y, n FilterKey) FilterResult {
return FilterResult{YesKey: y, NoKey: n}
}
func (f FilterKey) MatchOne() FilterResult {
return FilterResult{YesKey: f + 1, NoKey: f + 1}
}
// NeedData() is only really meaningful for ConsiderKey, and indicates
// that a decision can't be made from the key alone.
func (f FilterKey) NeedData() FilterResult {
return FilterResult{}
}
// Fail() reports a fatal error that should terminate processing.
func (f FilterKey) Fail(err error) FilterResult {
return FilterResult{Err: err}
}
// Failf() is just like Errorf, etc
func (f FilterKey) Failf(msg string, args ...interface{}) FilterResult {
return FilterResult{Err: fmt.Errorf(msg, args...)}
}
// MatchRow indicates that the current row matches the filter.
func (f FilterKey) MatchRow() FilterResult {
return FilterResult{YesKey: (f & rowMask) + rowWidth}
}
// MatchOneRejectRow indicates that this item matched but no further
// items in this row can match.
func (f FilterKey) MatchOneRejectRow() FilterResult {
return FilterResult{YesKey: f + 1, NoKey: (f & rowMask) + rowWidth}
}
// Reject rejects this item only.
func (f FilterKey) RejectOne() FilterResult {
return FilterResult{NoKey: f + 1}
}
// Reject rejects N items.
func (f FilterKey) Reject(n uint64) FilterResult {
return FilterResult{NoKey: f.Add(n)}
}
// RejectRow indicates that this entire row is rejected.
func (f FilterKey) RejectRow() FilterResult {
return FilterResult{NoKey: (f & rowMask) + rowWidth}
}
// RejectUntil rejects everything up to the given key.
func (f FilterKey) RejectUntil(until FilterKey) FilterResult {
return FilterResult{NoKey: until}
}
// RejectUntilRow rejects everything until the given row ID.
func (f FilterKey) RejectUntilRow(rowID uint64) FilterResult {
return FilterResult{NoKey: FilterKey(rowID) << rowExponent}
}
// MatchRowUntilRow matches this row, then rejects everything else until
// the given row ID.
func (f FilterKey) MatchRowUntilRow(rowID uint64) FilterResult {
// if rows are 16 wide, "yes" will be 16 minus our current position
// within a row, and "no" will be the distance from the end of our
// current row to the start of rowID, which is also the distance from
// the beginning of our current row to the start of rowID-1.
return FilterResult{
YesKey: (f & rowMask) + rowWidth,
NoKey: FilterKey(rowID) << rowExponent,
}
}
// RejectUntilOffset rejects this container, and any others until the given
// in-row offset.
func (f FilterKey) RejectUntilOffset(offset uint64) FilterResult {
next := (f & rowMask).Add(offset)
if next <= f {
next += rowWidth
}
return FilterResult{NoKey: next}
}
// MatchUntilOffset matches the current container, then skips any other
// containers until the given offset.
func (f FilterKey) MatchOneUntilOffset(offset uint64) FilterResult {
r := f.RejectUntilOffset(offset)
r.YesKey = f + 1
return r
}
// Done indicates that nothing can ever match.
func (f FilterKey) Done() FilterResult {
return FilterResult{
NoKey: ^FilterKey(0),
}
}
// MatchRowAndDone matches this row and nothing after that.
func (f FilterKey) MatchRowAndDone() FilterResult {
return FilterResult{
YesKey: (f & rowMask) + rowWidth,
NoKey: ^FilterKey(0),
}
}
// Match the current container, then skip any others until the same offset
// is reached again.
func (f FilterKey) MatchOneUntilSameOffset() FilterResult {
return f.MatchOneUntilOffset(uint64(f) & keyMask)
}
// A BitmapFilter, given a series of key/data pairs, is considered to "match"
// some of those containers. Matching may be dependent on key values and
// cardinalities alone, or on the contents of the container.
//
// The ConsiderData function must not retain the container, or the data
// from the container; if it needs access to that information later, it needs
// to make a copy.
//
// Many filters are, by virtue of how they operate, able to predict their
// results on future keys. To accommodate this, and allow operations to
// avoid processing keys they don't need to process, the result of a filter
// operation can indicate not just whether a given key matches, but whether
// some upcoming keys will, or won't, match. If ConsiderKey yields a non-zero
// number of matches or non-matches for a given key, ConsiderData will not be
// called for that key.
//
// If multiple filters are combined, they are only called if their input is
// needed to determine a value.
type BitmapFilter interface {
ConsiderKey(key FilterKey, n int32) FilterResult
ConsiderData(key FilterKey, data *Container) FilterResult
}
// BitmapColumnFilter is a BitmapFilter which checks for containers matching
// a given column within a row; thus, only the one container per row which
// matches the column needs to be evaluated, and it's evaluated as matching
// if it contains the relevant bit.
type BitmapColumnFilter struct {
key, offset uint16
}
var _ BitmapFilter = &BitmapColumnFilter{}
func (f *BitmapColumnFilter) ConsiderKey(key FilterKey, n int32) FilterResult {
if uint16(key&keyMask) != f.key {
return key.RejectUntilOffset(uint64(f.key))
}
return key.NeedData()
}
func (f *BitmapColumnFilter) ConsiderData(key FilterKey, data *Container) FilterResult {
if data.Contains(f.offset) {
return key.MatchOneUntilSameOffset()
}
return key.RejectUntilOffset(uint64(f.key))
}
func NewBitmapColumnFilter(col uint64) BitmapFilter {
return &BitmapColumnFilter{key: uint16((col >> 16) & keyMask), offset: uint16(col & 0xFFFF)}
}
// BitmapRowsFilter is a BitmapFilter which checks for containers that are
// in any of a provided list of rows. The row list should be sorted.
type BitmapRowsFilter struct {
rows []uint64
i int
}
func (f *BitmapRowsFilter) ConsiderKey(key FilterKey, n int32) FilterResult {
if f.i == -1 {
return key.Done()
}
if n == 0 {
return key.RejectOne()
}
row := uint64(key) >> rowExponent
for f.rows[f.i] < row {
f.i++
if f.i >= len(f.rows) {
f.i = -1
return key.Done()
}
}
if f.rows[f.i] > row {
return key.RejectUntilRow(f.rows[f.i])
}
// rows[f.i] must be equal, so we should match this row, until the
// next row, if there is a next row.
if f.i+1 < len(f.rows) {
return key.MatchRowUntilRow(f.rows[f.i+1])
}
return key.MatchRowAndDone()
}
func (f *BitmapRowsFilter) ConsiderData(key FilterKey, data *Container) FilterResult {
return key.Fail(errors.New("bitmap rows filter should never consider data"))
}
func NewBitmapRowsFilter(rows []uint64) BitmapFilter {
if len(rows) == 0 {
return &BitmapRowsFilter{rows: rows, i: -1}
}
return &BitmapRowsFilter{rows: rows, i: 0}
}
// BitmapRowFilterBase is a generic form of a row-aware wrapper; it
// handles making decisions about keys once you tell it a yesKey and noKey
// that it should be using, and makes callbacks per row.
type BitmapRowFilterBase struct {
FilterResult
callback func(row uint64) error
lastRow uint64
}
var _ BitmapFilter = &BitmapRowFilterBase{}
// DetermineByKey decides whether it can produce a meaningful FilterResult
// for a given key. This encapsulates all the logic for row callbacks and
// figuring out when to wrap a row.
func (b *BitmapRowFilterBase) DetermineByKey(key FilterKey) (FilterResult, bool) {
if b.FilterResult.Err != nil {
return b.FilterResult, true
}
row := key.Row()
if b.YesKey <= key && b.NoKey > key {
return key.RejectUntil(b.NoKey), true
}
if b.lastRow == row {
return key.RejectRow(), true
}
// If we got here: Either b.noKey is less than key, or b.yesKey is
// greater than key. If yesKey is greater, we match this row, and
// possibly update to mark that we've said no through to the end
// of this row.
if b.YesKey > key {
b.lastRow = row
if b.callback != nil {
err := b.callback(row)
if err != nil {
return key.Fail(err), true
}
}
res := key.MatchOneRejectRow()
// This is probably unnecessary, but the idea is, since
// we've decided that we're rejecting everything up to the
// end of this row, we want to be sure that a later call
// doesn't produce a different answer.
if b.NoKey < res.NoKey {
b.NoKey = res.NoKey
}
// if our run of yes answers ends before the rejected row
// ends, and our run of no answers extends beyond this row,
// we can reject until then. note that we can't round that
// up to a full row; if our inner filter were a column
// filter, for instance, that only wanted to see the 7th
// key in each row, we would want to reject up to that 7th
// key, but then look at it.
if b.YesKey <= res.NoKey && b.NoKey > res.NoKey {
res.NoKey = b.NoKey
}
return res, true
}
// Both keys are <= key, err is nil, so this is basically a
// NeedData.
return b.FilterResult, false
}
// SetResult is a convenience function so that things embedding this
// can just call this instead of using a long series of dotted names.
// It returns the new result of DetermineByKey after this change.
func (b *BitmapRowFilterBase) SetResult(key FilterKey, result FilterResult) FilterResult {
b.FilterResult = result
result, _ = b.DetermineByKey(key)
return result
}
// Without a sub-filter, we always-succeed; if we get a key that isn't
// already answered by our YesKey/NoKey/lastRow, we will match this key,
// reject the rest of the row, and update our keys accordingly. We'll
// also hit the callback, and return an error from it if appropriate.
func (b *BitmapRowFilterBase) ConsiderKey(key FilterKey, n int32) FilterResult {
var done bool
b.FilterResult, done = b.DetermineByKey(key)
if done {
return b.FilterResult
}
if n == 0 {
return key.RejectOne()
}
b.FilterResult = key.MatchOneRejectRow()
row := key.Row()
b.lastRow = row
if b.callback != nil {
b.Err = b.callback(row)
}
return b.FilterResult
}
// This should probably never be reached?
func (b *BitmapRowFilterBase) ConsiderData(key FilterKey, data *Container) FilterResult {
b.Err = errors.New("base iterator should never consider data")
return b.FilterResult
}
func NewBitmapRowFilterBase(callback func(row uint64) error) *BitmapRowFilterBase {
return &BitmapRowFilterBase{lastRow: ^uint64(0), callback: callback}
}
type BitmapRowLimitFilter struct {
BitmapRowFilterBase
limit uint64
}
var _ BitmapFilter = &BitmapRowLimitFilter{}
// Without a sub-filter, we always-succeed; if we get a key that isn't
// already answered by our YesKey/NoKey/lastRow, we will match the whole
// row.
func (b *BitmapRowLimitFilter) ConsiderKey(key FilterKey, n int32) FilterResult {
var done bool
b.FilterResult, done = b.DetermineByKey(key)
if done {
return b.FilterResult
}
if n == 0 {
return key.RejectOne()
}
if b.limit > 0 {
b.FilterResult = key.MatchRow()
b.limit--
} else {
b.FilterResult = key.Done()
}
return b.FilterResult
}
// This should probably never be reached?
func (b *BitmapRowLimitFilter) ConsiderData(key FilterKey, data *Container) FilterResult {
b.Err = errors.New("limit iterator should never consider data")
return b.FilterResult
}
func NewBitmapRowLimitFilter(limit uint64) *BitmapRowLimitFilter {
return &BitmapRowLimitFilter{BitmapRowFilterBase: *NewBitmapRowFilterBase(nil), limit: limit}
}
// BitmapRowFilterSingleFilter is a row iterator with a single
// filter, which is simpler than one with multiple filters where
// it coincidentally turns out that N==1.
type BitmapRowFilterSingleFilter struct {
BitmapRowFilterBase
filter BitmapFilter
}
func (b *BitmapRowFilterSingleFilter) ConsiderKey(key FilterKey, n int32) FilterResult {
res, done := b.DetermineByKey(key)
if done {
return res
}
return b.SetResult(key, b.filter.ConsiderKey(key, n))
}
func (b *BitmapRowFilterSingleFilter) ConsiderData(key FilterKey, data *Container) FilterResult {
// We already handled any consideration of the key above, in principle.
b.FilterResult = b.filter.ConsiderData(key, data)
if b.FilterResult.Err != nil {
return b.FilterResult
}
res, done := b.DetermineByKey(key)
if done {
return res
}
// We could just return the res, which would say nothing, but I
// think it should be a visible error if that happens.
b.FilterResult.Err = errors.New("inner filter didn't make a decision")
return b.FilterResult
}
func NewBitmapRowFilterSingleFilter(callback func(row uint64) error, filter BitmapFilter) *BitmapRowFilterSingleFilter {
return &BitmapRowFilterSingleFilter{
BitmapRowFilterBase: BitmapRowFilterBase{lastRow: ^uint64(0), callback: callback},
filter: filter,
}
}
// BitmapRowFilterMultiFilter is a BitmapFilter which wraps other bitmap filters,
// calling a callback function once per row whenever it finds a container
// for which all the filters returned true.
type BitmapRowFilterMultiFilter struct {
BitmapRowFilterBase
filters []BitmapFilter
yesKeys, noKeys []FilterKey
toDo []int
}
func (b *BitmapRowFilterMultiFilter) ConsiderKey(key FilterKey, n int32) FilterResult {
res, done := b.DetermineByKey(key)
if done {
return res
}
// highestNo: The highest No value that we have that isn't preceeded
// by a relevant Yes.
highestNo := key
lowestYes := ^FilterKey(0)
// The length of the "no" run after the lowest "yes"
lowestYesNo := FilterKey(0)
b.toDo = b.toDo[:0]
// We scan for any no values that don't have an earlier yes that's
// still greater than this key. If there are any, we can skip to
// the highest such value immediately. We also build a todo list
// of items for which we have neither a yes nor a no answer greater
// than this key.
for i, yk := range b.yesKeys {
if yk > key {
if yk < lowestYes {
lowestYes = yk
lowestYesNo = b.noKeys[i]
}
continue
}
nk := b.noKeys[i]
if nk > highestNo {
highestNo = nk
continue
}
b.toDo = append(b.toDo, i)
}
// We have an unambiguous no, so we can set our internal state to
// be aware that we have a No until then. We can unconditionally
// return the result; it can't be not-done, because we just set
// it to a known done state.
if highestNo > key {
return b.SetResult(key, key.RejectUntil(highestNo))
}
// Everything either has a yes value which is at least as high
// as lowestYes, or is in f.toDo now. Now we call ConsiderKey
// for everything in f.toDo, and accumulate a new list of the
// values still don't know, using the same backing store.
newToDo := b.toDo[:0]
for _, filter := range b.toDo {
result := b.filters[filter].ConsiderKey(key, n)
if result.Err != nil {
return key.Fail(result.Err)
}
yk, nk := result.YesKey, result.NoKey
b.yesKeys[filter], b.noKeys[filter] = yk, nk
if yk > key {
if lowestYes == 0 || yk < lowestYes {
lowestYes = yk
lowestYesNo = nk
}
continue
}
if nk > highestNo {
highestNo = nk
continue
}
newToDo = append(newToDo, filter)
}
// Same logic as before; if we have a highestNo, we don't need more
// information.
if highestNo > key {
return b.SetResult(key, key.RejectUntil(highestNo))
}
b.toDo = newToDo
if len(b.toDo) > 0 {
return key.NeedData()
}
// this shouldn't be possible
if lowestYes <= key {
return key.Failf("got lowest yes %d for key %d, this shouldn't happen", lowestYes, key)
}
// Flag that we have a definite Yes as far as the lowest yes, and a
// definite No after that to the corresponding No.
return b.SetResult(key, key.MatchReject(lowestYes, lowestYesNo))
}
// ConsiderData only gets called in cases where f.toDo had a list of filters
// for which we needed to get data to make a decision. That means that
// everything but the indexes in f.toDo must be a "yes" right now.
func (b *BitmapRowFilterMultiFilter) ConsiderData(key FilterKey, data *Container) FilterResult {
res, done := b.DetermineByKey(key)
if done {
return res
}
highestNo := key
for _, filter := range b.toDo {
result := b.filters[filter].ConsiderData(key, data)
if result.Err != nil {
return key.Fail(result.Err)
}
yk, nk := result.YesKey, result.NoKey
b.yesKeys[filter], b.noKeys[filter] = yk, nk
if yk <= key && nk > highestNo {
highestNo = nk
}
}
if highestNo > key {
return b.SetResult(key, key.RejectUntil(highestNo))
}
// if we got here, either something was buggy, or everything has a yes
// > key.
lowestYes := ^FilterKey(0)
lowestYesNo := key
for i, yk := range b.yesKeys {
if yk < lowestYes {
lowestYes = yk
lowestYesNo = b.noKeys[i]
}
}
// this shouldn't be possible
if lowestYes <= key {
return key.Failf("got lowest yes %d on data for key %d, this shouldn't happen", lowestYes, key)
}
return b.SetResult(key, key.MatchReject(lowestYes, lowestYesNo))
}
// BitmapBitmap filter builds a list of positions in the bitmap which
// match those in a provided bitmap. It is shard-agnostic; no matter what
// offsets the input bitmap's containers have, it matches them against
// corresponding keys.
type BitmapBitmapFilter struct {
filter *Bitmap // We don't use this while iterating, but in ludicrous edge cases it might be holding a generation we need.
containers []*Container
nextOffsets []uint64
callback func(uint64) error
}
func (b *BitmapBitmapFilter) ConsiderKey(key FilterKey, n int32) FilterResult {
pos := key & keyMask
if b.containers[pos] == nil || n == 0 {
return key.RejectUntilOffset(b.nextOffsets[pos])
}
return key.NeedData()
}
func (b *BitmapBitmapFilter) ConsiderData(key FilterKey, data *Container) FilterResult {
pos := key & keyMask
base := uint64(key << 16)
filter := b.containers[pos]
if filter == nil {
key.RejectUntilOffset(b.nextOffsets[pos])
}
var lastErr error
matched := false
intersectionCallback(data, filter, func(v uint16) {
matched = true
err := b.callback(base + uint64(v))
if err != nil {
lastErr = err
}
})
if lastErr != nil {
return key.Fail(lastErr)
}
if !matched {
return key.RejectUntilOffset(b.nextOffsets[pos])
}
return key.MatchOneUntilOffset(b.nextOffsets[pos])
}
// NewBitmapBitmapFilter creates a filter which can report all the positions
// within a bitmap which are set, and which have positions corresponding to
// the specified columns. It calls the provided callback function on
// each value it finds, terminating early if that returns an error.
//
// The input filter is assumed to represent one "row" of a shard's data,
// which is to say, a range of up to rowWidth consecutive containers starting
// at some multiple of rowWidth. We coerce that to the 0..rowWidth range
// because offset-within-row is what we care about.
func NewBitmapBitmapFilter(filter *Bitmap, callback func(uint64) error) *BitmapBitmapFilter {
b := &BitmapBitmapFilter{
filter: filter,
callback: callback,
containers: make([]*Container, rowWidth),
nextOffsets: make([]uint64, rowWidth),
}
iter, _ := filter.Containers.Iterator(0)
last := uint64(0)
count := 0
for iter.Next() {
k, v := iter.Value()
// Coerce container key into the 0-rowWidth range we'll be
// using to compare against containers within each row.
k = k & keyMask
b.containers[k] = v
last = k
count++
}
// if there's only one container, we need to populate everything with
// its position.
if count == 1 {
for i := range b.containers {
b.nextOffsets[i] = last
}
} else {
// Point each container at the offset of the next valid container.
// With sparse bitmaps this will potentially make skipping faster.
for i := range b.containers {
if b.containers[i] != nil {
for int(last) != i {
b.nextOffsets[last] = uint64(i)
last = (last + 1) % rowWidth
}
}
}
}
return b
}
// BitmapRowFilterMultiFilter will call a
func NewBitmapRowFilterMultiFilter(callback func(row uint64) error, filters ...BitmapFilter) BitmapFilter {
return &BitmapRowFilterMultiFilter{
filters: filters,
yesKeys: make([]FilterKey, len(filters)),
noKeys: make([]FilterKey, len(filters)),
BitmapRowFilterBase: BitmapRowFilterBase{
callback: callback,
lastRow: ^uint64(0),
},
}
}
// BitmapRowLister returns a pointer to a slice which it will populate when invoked
// as a bitmap filter.
func NewBitmapRowFilter(callback func(uint64) error, filters ...BitmapFilter) BitmapFilter {
if len(filters) == 0 {
return NewBitmapRowFilterBase(callback)
}
if len(filters) == 1 {
return NewBitmapRowFilterSingleFilter(callback, filters[0])
}
return NewBitmapRowFilterMultiFilter(callback, filters...)
}
// BitmapRangeFilter limits filter operations to a specified range, and
// performs key or data callbacks.
//
// On seeing a key in its range:
// If the key callback is present, and returns true, match the key.
// Otherwise, if a data callback is present, request the data, and in the
// data handler, call the data callback, then match the single key.
// If neither is present, match the entire range at once.
type BitmapRangeFilter struct {
min, max FilterKey
kcb func(FilterKey, int32) (bool, error)
dcb func(FilterKey, *Container) error
}
var _ BitmapFilter = &BitmapRangeFilter{}
func (b *BitmapRangeFilter) ConsiderKey(key FilterKey, n int32) FilterResult {
if key >= b.max {
return key.Done()
}
if key >= b.min {
if b.kcb != nil {
match, err := b.kcb(key, n)
if err != nil {
return key.Fail(err)
}
if match {
return key.MatchOne()
}
}
if b.dcb != nil {
return key.NeedData()
}
return key.MatchReject(b.max, ^FilterKey(0))
}
return key.RejectUntil(b.min)
}
func (b *BitmapRangeFilter) ConsiderData(key FilterKey, data *Container) FilterResult {
err := b.dcb(key, data)
if err != nil {
return key.Fail(err)
}
return key.MatchOne()
}
func NewBitmapRangeFilter(min, max FilterKey, keyCallback func(FilterKey, int32) (bool, error), dataCallback func(FilterKey, *Container) error) *BitmapRangeFilter {
return &BitmapRangeFilter{min: min, max: max, kcb: keyCallback, dcb: dataCallback}
}
// BitmapMutexDupFilter is a filter which identifies cases where the same
// position has a bit set in more than one row.
//
// We keep a slice of the first value seen for every row, with ^0 as the
// default; when that's already set, things get appended to the entries in
// the map. At the end, for each entry in the map, we also add its first
// value to it. Thus, the map holds all the entries, but we're only using
// the map in the (hopefully rarer) cases where there's duplicate values.
//
// The slice is local-coordinates (first column 0), but the map is global
// coordinates (first column is whatever base was).
type BitmapMutexDupFilter struct {
base uint64 // the offset of 0 for this, used to accommodate shard offsets
extra map[uint64][]uint64 // extra values observed
first []uint64 // first values observed
details bool
limit int
done bool // if we have a limit, and we've hit it...
highKey FilterKey // ... we can stop after this many containers.
}
var _ BitmapFilter = &BitmapMutexDupFilter{}
func NewBitmapMutexDupFilter(base uint64, details bool, limit int) *BitmapMutexDupFilter {
filter := &BitmapMutexDupFilter{
base: base,
extra: map[uint64][]uint64{},
first: make([]uint64, 1<<shardwidth.Exponent),
details: details,
limit: limit,
}
if filter.limit == 0 {
// A limit of 0 is not a limit; set limit higher than possible number of
// values we could have.
filter.limit = 2 << shardwidth.Exponent
}
for i := range filter.first {
filter.first[i] = ^uint64(0)
}
return filter
}
func (b *BitmapMutexDupFilter) ConsiderKey(key FilterKey, n int32) FilterResult {
if n > 0 {
return key.NeedData()
}
return key.RejectOne()
}
func (b *BitmapMutexDupFilter) ConsiderData(key FilterKey, data *Container) FilterResult {
value, basePos := uint64(key)>>rowExponent, uint64(key&keyMask)<<16
containerCallback(data, func(u uint16) {
pos := basePos + uint64(u)
if b.first[pos] != ^uint64(0) {
if b.details {
b.extra[pos+b.base] = append(b.extra[pos+b.base], value)
} else {
// no details, just annotate that it exists
b.extra[pos+b.base] = []uint64{}
}
} else {
b.first[pos] = value
}
})
if len(b.extra) >= b.limit {
if !b.done {
// we note which container we found the last value we needed in.
// We may still go over the limit, but we won't look at any *more*
// containers in this row.
//
// We can't just abort early because the records we already found
// could have more values.
b.done = true
b.highKey = key & keyMask
return key.RejectRow()
}
if (key & keyMask) >= b.highKey {
return key.RejectRow()
}
}
return key.MatchOne()
}
// Report returns the set of duplicate values identified.
func (b *BitmapMutexDupFilter) Report() map[uint64][]uint64 {
// copy values into extra, and remove them from first, so calling
// Report() again won't cause double-appends. We only have to do
// this if we've been asked for details; otherwise the list of
// known positions is sufficient.
if b.details {
for k, v := range b.extra {
kpos := k % (1 << shardwidth.Exponent)
if b.first[kpos] != ^uint64(0) {
v = append(v, 0)
// prepend so the lowest value goes at the beginning
copy(v[1:], v[:])
v[0] = b.first[kpos]
b.first[kpos] = ^uint64(0)
b.extra[k] = v
}
}
}
return b.extra
}
// ApplyFilterToIterator is a simplistic implementation that applies a bitmap
// filter to a ContainerIterator, returning an error if it encounters an error.
//
// This mostly exists for testing purposes; a Tx implementation where generating
// containers is expensive should almost certainly implement a better way to
// use filters which only generates data if it needs to.
func ApplyFilterToIterator(filter BitmapFilter, iter ContainerIterator) error {
defer iter.Close()
var until = uint64(0)
for (until < ^uint64(0)) && iter.Next() {
key, data := iter.Value()
if key < until {
continue
}
result := filter.ConsiderKey(FilterKey(key), data.N())
if result.Err != nil {
return result.Err
}
until = uint64(result.NoKey)
if key < until {
continue
}
result = filter.ConsiderData(FilterKey(key), data)
if result.Err != nil {
return result.Err
}
until = uint64(result.NoKey)
}
return nil
}