mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
We switch everything to use QueryContext/QueryRead/etc instead of Qcx/Tx. We drop the short_txkey subpackage (it's now handled by either keys or querycontext). We drop all the dbshard stuff, and all the tx/txfactory stuff. We remove all the things that related to the old "Block" concept, which was mostly used by the anti-entropy code, but had one fragmentary usage left in the ImportRoaringOverwrite case of ImportRoaring. That's replaced by using a rewriter that deletes all bits (not just bits in specific columns) from an existing thing, but writes in new bits. Actually we could probably do that better with a custom "eradicate-rewriter" that doesn't try to be clever, and just eliminates things. This includes a number of minor bug fixes that were exposed by getting the testing to work. For example: * When checking whether an operation "requires write", we now consider a Delete a kind of a Write, because it is. * Several tests were relying on the fact that writes through Qcx were being committed whether or not the Qcx was ever told to finish. With QueryContext, you actually have to reach a Commit() or the writes don't happen (except for special cases in Delete). * Replaced a lot of panics with t.Fatalf in tests. There's also some minor staticcheck fixes, like deleting the unused "db" member of a boltdb transaction wrapper.
554 lines
16 KiB
Go
554 lines
16 KiB
Go
// Copyright 2021 Molecula Corp. All rights reserved.
|
|
package pilosa
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/bits"
|
|
"reflect"
|
|
"time"
|
|
|
|
"github.com/molecula/featurebase/v3/shardwidth"
|
|
"github.com/molecula/featurebase/v3/tracing"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// QueryRequest represent a request to process a query.
|
|
type QueryRequest struct {
|
|
// Index to execute query against.
|
|
Index string
|
|
|
|
// The query string to parse and execute.
|
|
Query string
|
|
|
|
// The SQL source query, if applicable.
|
|
SQLQuery string
|
|
|
|
// The shards to include in the query execution.
|
|
// If empty, all shards are included.
|
|
Shards []uint64
|
|
|
|
// If true, indicates that query is part of a larger distributed query.
|
|
// If false, this request is on the originating node.
|
|
Remote bool
|
|
|
|
// Query has already been translated. This is only used if Remote
|
|
// is false, Remote=true implies this.
|
|
PreTranslated bool
|
|
|
|
// Should we profile this query?
|
|
Profile bool
|
|
|
|
// Additional data associated with the query, in cases where there's
|
|
// row-style inputs for precomputed values.
|
|
EmbeddedData []*Row
|
|
|
|
// Limit on memory used by request (Extract() only)
|
|
MaxMemory int64
|
|
}
|
|
|
|
// QueryResponse represent a response from a processed query.
|
|
type QueryResponse struct {
|
|
// Result for each top-level query call.
|
|
// The result type differs depending on the query; types
|
|
// include: Row, RowIdentifiers, GroupCounts, SignedRow,
|
|
// ValCount, Pair, Pairs, bool, uint64.
|
|
Results []interface{}
|
|
|
|
// Error during parsing or execution.
|
|
Err error
|
|
|
|
// Profiling data, if any
|
|
Profile *tracing.Profile
|
|
}
|
|
|
|
// MarshalJSON marshals QueryResponse into a JSON-encoded byte slice
|
|
func (resp *QueryResponse) MarshalJSON() ([]byte, error) {
|
|
if resp.Err != nil {
|
|
return json.Marshal(struct {
|
|
Err string `json:"error"`
|
|
}{Err: resp.Err.Error()})
|
|
}
|
|
|
|
return json.Marshal(struct {
|
|
Results []interface{} `json:"results"`
|
|
Profile *tracing.Profile `json:"profile,omitempty"`
|
|
}{
|
|
Results: resp.Results,
|
|
Profile: resp.Profile,
|
|
})
|
|
}
|
|
|
|
// SameAs compares one QueryResponse to another and returns nil if the Results
|
|
// and Err are both identical, or a descriptive error if they differ.
|
|
// This function replaces using reflect.DeepEqual directly on the
|
|
// QueryResponse, since QueryResponse contains an error field and reflect.DeepEqual
|
|
// should not be used on errors.
|
|
func (qr *QueryResponse) SameAs(other *QueryResponse) error {
|
|
switch {
|
|
case !reflect.DeepEqual(qr.Results, other.Results):
|
|
return fmt.Errorf("responses contained different results")
|
|
case qr.Err == nil && other.Err == nil:
|
|
return nil
|
|
case qr.Err == nil && other.Err != nil:
|
|
return fmt.Errorf("unexpected error: %w", other.Err)
|
|
case qr.Err != nil && other.Err == nil:
|
|
return fmt.Errorf("missing error: expected %v, got no error", qr.Err)
|
|
case qr.Err.Error() != other.Err.Error():
|
|
return fmt.Errorf("wrong error: expected %v, got %w", qr.Err, other.Err)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// HandlerI is the interface for the data handler, a wrapper around
|
|
// Pilosa's data store.
|
|
type HandlerI interface {
|
|
Serve() error
|
|
Close() error
|
|
}
|
|
|
|
type nopHandler struct{}
|
|
|
|
func (n nopHandler) Serve() error {
|
|
return nil
|
|
}
|
|
|
|
func (n nopHandler) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// NopHandler is a no-op implementation of the Handler interface.
|
|
var NopHandler HandlerI = nopHandler{}
|
|
|
|
// ImportValueRequest describes the import request structure
|
|
// for a value (BSI) import.
|
|
// Note: no RowIDs here. have to convert BSI Values into RowIDs internally.
|
|
type ImportValueRequest struct {
|
|
Index string
|
|
IndexCreatedAt int64
|
|
Field string
|
|
FieldCreatedAt int64
|
|
// if Shard is MaxUint64 (an impossible shard value), this
|
|
// indicates that the column IDs may come from multiple shards.
|
|
Shard uint64
|
|
ColumnIDs []uint64 // e.g. weather stationID
|
|
ColumnKeys []string
|
|
Values []int64 // e.g. temperature, humidity, barometric pressure
|
|
FloatValues []float64
|
|
TimestampValues []time.Time
|
|
StringValues []string
|
|
Clear bool
|
|
scratch []int // scratch space to allow us to get a stable sort in reasonable time
|
|
}
|
|
|
|
func (ivr *ImportValueRequest) Clone() *ImportValueRequest {
|
|
newIVR := &ImportValueRequest{}
|
|
if ivr == nil {
|
|
return newIVR
|
|
}
|
|
*newIVR = *ivr
|
|
// don't copy the internal scratch buffer
|
|
newIVR.scratch = nil
|
|
if len(ivr.ColumnIDs) > 0 {
|
|
newIVR.ColumnIDs = make([]uint64, len(ivr.ColumnIDs))
|
|
copy(newIVR.ColumnIDs, ivr.ColumnIDs)
|
|
}
|
|
if len(ivr.ColumnKeys) > 0 {
|
|
newIVR.ColumnKeys = make([]string, len(ivr.ColumnKeys))
|
|
copy(newIVR.ColumnKeys, ivr.ColumnKeys)
|
|
}
|
|
if len(ivr.Values) > 0 {
|
|
newIVR.Values = make([]int64, len(ivr.Values))
|
|
copy(newIVR.Values, ivr.Values)
|
|
}
|
|
if len(ivr.FloatValues) > 0 {
|
|
newIVR.FloatValues = make([]float64, len(ivr.FloatValues))
|
|
copy(newIVR.FloatValues, ivr.FloatValues)
|
|
}
|
|
if len(ivr.TimestampValues) > 0 {
|
|
newIVR.TimestampValues = make([]time.Time, len(ivr.TimestampValues))
|
|
copy(newIVR.TimestampValues, ivr.TimestampValues)
|
|
}
|
|
if len(ivr.StringValues) > 0 {
|
|
newIVR.StringValues = make([]string, len(ivr.StringValues))
|
|
copy(newIVR.StringValues, ivr.StringValues)
|
|
}
|
|
return newIVR
|
|
}
|
|
|
|
// AtomicRecord applies all its Ivr and Ivr atomically, in a Tx.
|
|
// The top level Shard has to agree with Ivr[i].Shard and the Iv[i].Shard
|
|
// for all i included (in Ivr and Ir). The same goes for the top level Index: all records
|
|
// have to be writes to the same Index. These requirements are checked.
|
|
type AtomicRecord struct {
|
|
Index string
|
|
Shard uint64
|
|
|
|
Ivr []*ImportValueRequest // BSI values
|
|
Ir []*ImportRequest // other field types, e.g. single bit
|
|
}
|
|
|
|
func (ar *AtomicRecord) Clone() *AtomicRecord {
|
|
newAR := &AtomicRecord{Index: ar.Index, Shard: ar.Shard}
|
|
newAR.Ivr = make([]*ImportValueRequest, len(ar.Ivr))
|
|
for i, vr := range ar.Ivr {
|
|
newAR.Ivr[i] = vr.Clone()
|
|
}
|
|
newAR.Ir = make([]*ImportRequest, len(ar.Ir))
|
|
for i, vr := range ar.Ir {
|
|
newAR.Ir[i] = vr.Clone()
|
|
}
|
|
return newAR
|
|
}
|
|
|
|
func (ivr *ImportValueRequest) Len() int { return len(ivr.ColumnIDs) }
|
|
func (ivr *ImportValueRequest) Less(i, j int) bool {
|
|
if ivr.ColumnIDs[i] < ivr.ColumnIDs[j] {
|
|
return true
|
|
}
|
|
if ivr.ColumnIDs[i] > ivr.ColumnIDs[j] {
|
|
return false
|
|
}
|
|
if len(ivr.scratch) > 0 {
|
|
return ivr.scratch[i] < ivr.scratch[j]
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (ivr *ImportValueRequest) Swap(i, j int) {
|
|
ivr.ColumnIDs[i], ivr.ColumnIDs[j] = ivr.ColumnIDs[j], ivr.ColumnIDs[i]
|
|
if len(ivr.Values) > 0 {
|
|
ivr.Values[i], ivr.Values[j] = ivr.Values[j], ivr.Values[i]
|
|
} else if len(ivr.FloatValues) > 0 {
|
|
ivr.FloatValues[i], ivr.FloatValues[j] = ivr.FloatValues[j], ivr.FloatValues[i]
|
|
} else if len(ivr.TimestampValues) > 0 {
|
|
ivr.TimestampValues[i], ivr.TimestampValues[j] = ivr.TimestampValues[j], ivr.TimestampValues[i]
|
|
} else if len(ivr.StringValues) > 0 {
|
|
ivr.StringValues[i], ivr.StringValues[j] = ivr.StringValues[j], ivr.StringValues[i]
|
|
}
|
|
if len(ivr.scratch) > 0 {
|
|
ivr.scratch[i], ivr.scratch[j] = ivr.scratch[j], ivr.scratch[i]
|
|
}
|
|
}
|
|
|
|
// Validate ensures that the payload of the request is valid.
|
|
func (ivr *ImportValueRequest) Validate() error {
|
|
return ivr.ValidateWithTimestamp(ivr.IndexCreatedAt, ivr.FieldCreatedAt)
|
|
}
|
|
|
|
// ValidateWithTimestamp ensures that the payload of the request is valid.
|
|
func (ivr *ImportValueRequest) ValidateWithTimestamp(indexCreatedAt, fieldCreatedAt int64) error {
|
|
if ivr.Index == "" || ivr.Field == "" {
|
|
return errors.Errorf("index and field required, but got '%s' and '%s'", ivr.Index, ivr.Field)
|
|
}
|
|
if len(ivr.ColumnIDs) != 0 && len(ivr.ColumnKeys) != 0 {
|
|
return errors.Errorf("must pass either column ids or keys, but not both")
|
|
}
|
|
var valueSetCount int
|
|
if len(ivr.Values) != 0 {
|
|
valueSetCount++
|
|
}
|
|
if len(ivr.FloatValues) != 0 {
|
|
valueSetCount++
|
|
}
|
|
if len(ivr.TimestampValues) != 0 {
|
|
valueSetCount++
|
|
}
|
|
if len(ivr.StringValues) != 0 {
|
|
valueSetCount++
|
|
}
|
|
if valueSetCount > 1 {
|
|
return errors.Errorf("must pass ints, floats, or strings but not multiple")
|
|
}
|
|
|
|
if (ivr.IndexCreatedAt != 0 && ivr.IndexCreatedAt != indexCreatedAt) ||
|
|
(ivr.FieldCreatedAt != 0 && ivr.FieldCreatedAt != fieldCreatedAt) {
|
|
return ErrPreconditionFailed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ImportRequest describes the import request structure
|
|
// for an import. BSIs use the ImportValueRequest instead.
|
|
type ImportRequest struct {
|
|
Index string
|
|
IndexCreatedAt int64
|
|
Field string
|
|
FieldCreatedAt int64
|
|
Shard uint64
|
|
RowIDs []uint64
|
|
ColumnIDs []uint64
|
|
RowKeys []string
|
|
ColumnKeys []string
|
|
Timestamps []int64
|
|
Clear bool
|
|
}
|
|
|
|
// Clone allows copying an import request. Normally you wouldn't, but
|
|
// some import functions are destructive on their inputs, and if you
|
|
// want to *re-use* an import request, you might need this. If you're
|
|
// using this outside tx_test, something is probably wrong.
|
|
func (ir *ImportRequest) Clone() *ImportRequest {
|
|
newIR := &ImportRequest{}
|
|
if ir == nil {
|
|
return newIR
|
|
}
|
|
*newIR = *ir
|
|
if ir.RowIDs != nil {
|
|
newIR.RowIDs = make([]uint64, len(ir.RowIDs))
|
|
copy(newIR.RowIDs, ir.RowIDs)
|
|
}
|
|
if ir.ColumnIDs != nil {
|
|
newIR.ColumnIDs = make([]uint64, len(ir.ColumnIDs))
|
|
copy(newIR.ColumnIDs, ir.ColumnIDs)
|
|
}
|
|
if ir.RowKeys != nil {
|
|
newIR.RowKeys = make([]string, len(ir.RowKeys))
|
|
copy(newIR.RowKeys, ir.RowKeys)
|
|
}
|
|
if ir.ColumnKeys != nil {
|
|
newIR.ColumnKeys = make([]string, len(ir.ColumnKeys))
|
|
copy(newIR.ColumnKeys, ir.ColumnKeys)
|
|
}
|
|
if ir.Timestamps != nil {
|
|
newIR.Timestamps = make([]int64, len(ir.Timestamps))
|
|
copy(newIR.Timestamps, ir.Timestamps)
|
|
}
|
|
return newIR
|
|
}
|
|
|
|
// SortToShards takes an import request which has been translated, but may
|
|
// not be sorted, and turns it into a map from shard IDs to individual import
|
|
// requests. We don't sort the entries within each shard because the correct
|
|
// sorting depends on the field type and we don't want to deal with that
|
|
// here.
|
|
func (ir *ImportRequest) SortToShards() (result map[uint64]*ImportRequest) {
|
|
if len(ir.ColumnIDs) == 0 {
|
|
return nil
|
|
}
|
|
diffMask := uint64(0)
|
|
prev := ir.ColumnIDs[0]
|
|
for _, r := range ir.ColumnIDs[1:] {
|
|
diffMask |= r ^ prev
|
|
prev = r
|
|
}
|
|
bitsRemaining := bits.Len64(diffMask)
|
|
if bitsRemaining <= shardwidth.Exponent {
|
|
shard := ir.ColumnIDs[0] >> shardwidth.Exponent
|
|
ir.Shard = shard
|
|
ir.ColumnKeys = nil
|
|
ir.RowKeys = nil
|
|
return map[uint64]*ImportRequest{shard: ir}
|
|
}
|
|
output := make(map[uint64]*ImportRequest)
|
|
sortToShardsInto(ir, bitsRemaining-8, output)
|
|
return output
|
|
}
|
|
|
|
// sortToShardsInto puts the shards it finds into the given map, so that
|
|
// as we split off buckets, they can be inserted into the same map.
|
|
func sortToShardsInto(ir *ImportRequest, shift int, into map[uint64]*ImportRequest) {
|
|
if shift < shardwidth.Exponent {
|
|
shift = shardwidth.Exponent
|
|
}
|
|
nextShift := shift - 8
|
|
if nextShift < shardwidth.Exponent {
|
|
nextShift = shardwidth.Exponent
|
|
}
|
|
// count things that belong in each of the 256 buckets
|
|
var buckets [256]int
|
|
var starts [256]int
|
|
|
|
// compute the buckets ourselves
|
|
for _, r := range ir.ColumnIDs {
|
|
b := (r >> shift) & 0xFF
|
|
buckets[b]++
|
|
}
|
|
total := 0
|
|
// compute starting points of each bucket, converting the
|
|
// bucket counts into ends
|
|
for i := range buckets {
|
|
starts[i] = total
|
|
total += buckets[i]
|
|
buckets[i] = total
|
|
}
|
|
// starts[n] is the index of the first thing that should
|
|
// go in that bucket, buckets[n] is the index of the first
|
|
// thing that shouldn't
|
|
var bucketOp ImportRequest = *ir
|
|
bucketOp.ColumnKeys = nil
|
|
bucketOp.RowKeys = nil
|
|
origStarts := make([]int, len(starts))
|
|
copy(origStarts, starts[:])
|
|
for bucket, start := range origStarts {
|
|
end := buckets[bucket]
|
|
if end <= start {
|
|
continue
|
|
}
|
|
for j := start; j < end; j++ {
|
|
want := int((ir.ColumnIDs[j] >> shift) & 0xFF)
|
|
for want != bucket {
|
|
// move this to the beginning of the
|
|
// bucket it wants to be in, swapping
|
|
// the thing there here
|
|
dst := starts[want]
|
|
ir.ColumnIDs[j], ir.ColumnIDs[dst] = ir.ColumnIDs[dst], ir.ColumnIDs[j]
|
|
if ir.RowIDs != nil {
|
|
ir.RowIDs[j], ir.RowIDs[dst] = ir.RowIDs[dst], ir.RowIDs[j]
|
|
}
|
|
if ir.Timestamps != nil {
|
|
ir.Timestamps[j], ir.Timestamps[dst] = ir.Timestamps[dst], ir.Timestamps[j]
|
|
}
|
|
starts[want]++
|
|
want = int((ir.ColumnIDs[j] >> shift) & 0xFF)
|
|
}
|
|
}
|
|
// If shift == shardwidth.Exponent, then this is a completed
|
|
// shard and can go into the sharded output. otherwise, we
|
|
// can subdivide it.
|
|
bucketOp.ColumnIDs = ir.ColumnIDs[start:end]
|
|
if ir.RowIDs != nil {
|
|
bucketOp.RowIDs = ir.RowIDs[start:end]
|
|
}
|
|
if ir.Timestamps != nil {
|
|
bucketOp.Timestamps = ir.Timestamps[start:end]
|
|
}
|
|
if shift == shardwidth.Exponent {
|
|
x := bucketOp
|
|
shard := ir.ColumnIDs[start] >> shardwidth.Exponent
|
|
x.Shard = shard
|
|
into[shard] = &x
|
|
} else {
|
|
sortToShardsInto(&bucketOp, nextShift, into)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ValidateWithTimestamp ensures that the payload of the request is valid.
|
|
func (ir *ImportRequest) ValidateWithTimestamp(indexCreatedAt, fieldCreatedAt int64) error {
|
|
if (ir.IndexCreatedAt != 0 && ir.IndexCreatedAt != indexCreatedAt) ||
|
|
(ir.FieldCreatedAt != 0 && ir.FieldCreatedAt != fieldCreatedAt) {
|
|
return ErrPreconditionFailed
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
RequestActionSet = "set"
|
|
RequestActionClear = "clear"
|
|
RequestActionOverwrite = "overwrite"
|
|
)
|
|
|
|
// ImportRoaringRequest describes the import request structure
|
|
// for an import containing roaring-encoded data.
|
|
type ImportRoaringRequest struct {
|
|
IndexCreatedAt int64
|
|
FieldCreatedAt int64
|
|
Clear bool
|
|
Action string // [set, clear, overwrite]
|
|
Views map[string][]byte
|
|
UpdateExistence bool
|
|
SuppressLog bool
|
|
}
|
|
|
|
// ImportRoaringShardRequest is the request for the shard
|
|
// transactional endpoint.
|
|
type ImportRoaringShardRequest struct {
|
|
// Has this request already been forwarded to all replicas? If
|
|
// Remote=false, then the handling server is responsible for
|
|
// ensuring this request is sent to all repliacs before returning
|
|
// a successful response to the client.
|
|
Remote bool
|
|
Views []RoaringUpdate
|
|
|
|
// SuppressLog requests we not write to the write log. Typically
|
|
// that would be because this request is being replayed from a
|
|
// write log.
|
|
SuppressLog bool
|
|
}
|
|
|
|
// RoaringUpdate represents the bits to clear and then set in a particular view.
|
|
type RoaringUpdate struct {
|
|
Field string
|
|
View string
|
|
|
|
// Clear is a roaring encoded bitmatrix of bits to clear. For
|
|
// mutex or int-like fields, only the first row is looked at and
|
|
// the bits in that row are cleared from every row.
|
|
Clear []byte
|
|
|
|
// Set is the roaring encoded bitmatrix of bits to set. If this is
|
|
// a mutex or int-like field, we'll assume the first shard width
|
|
// of containers is the exists row and we will first clear all
|
|
// bits in those columns and then set
|
|
Set []byte
|
|
|
|
// ClearRecords, when true, denotes that Clear should be
|
|
// interpreted as a single row which will be subtracted from every
|
|
// row in this view.
|
|
ClearRecords bool
|
|
}
|
|
|
|
// ValidateWithTimestamp ensures that the payload of the request is valid.
|
|
func (irr *ImportRoaringRequest) ValidateWithTimestamp(indexCreatedAt, fieldCreatedAt int64) error {
|
|
if (irr.IndexCreatedAt != 0 && irr.IndexCreatedAt != indexCreatedAt) ||
|
|
(irr.FieldCreatedAt != 0 && irr.FieldCreatedAt != fieldCreatedAt) {
|
|
return ErrPreconditionFailed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ImportResponse is the structured response of an import.
|
|
type ImportResponse struct {
|
|
Err string
|
|
}
|
|
|
|
// BlockDataRequest describes the structure of a request
|
|
// for fragment block data.
|
|
type BlockDataRequest struct {
|
|
Index string
|
|
Field string
|
|
View string
|
|
Shard uint64
|
|
Block uint64
|
|
}
|
|
|
|
// BlockDataResponse is the structured response of a block
|
|
// data request.
|
|
type BlockDataResponse struct {
|
|
RowIDs []uint64
|
|
ColumnIDs []uint64
|
|
}
|
|
|
|
// TranslateKeysRequest describes the structure of a request
|
|
// for a batch of key translations.
|
|
type TranslateKeysRequest struct {
|
|
Index string
|
|
Field string
|
|
Keys []string
|
|
|
|
// NotWritable is an awkward name, but it's just to keep backward compatibility with client and idk.
|
|
NotWritable bool
|
|
}
|
|
|
|
// TranslateKeysResponse is the structured response of a key
|
|
// translation request.
|
|
type TranslateKeysResponse struct {
|
|
IDs []uint64
|
|
}
|
|
|
|
// TranslateIDsRequest describes the structure of a request
|
|
// for a batch of id translations.
|
|
type TranslateIDsRequest struct {
|
|
Index string
|
|
Field string
|
|
IDs []uint64
|
|
}
|
|
|
|
// TranslateIDsResponse is the structured response of a id
|
|
// translation request.
|
|
type TranslateIDsResponse struct {
|
|
Keys []string
|
|
}
|