mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-07 09:05:55 +00:00
Merge branch 'master' into bounds-check
This commit is contained in:
commit
eb7af13be2
42 changed files with 1784 additions and 262 deletions
2
Makefile
2
Makefile
|
|
@ -110,7 +110,7 @@ docker-test:
|
|||
# Run gometalinter with custom flags
|
||||
gometalinter: require-gometalinter
|
||||
gometalinter --vendor --disable-all \
|
||||
--deadline=120s \
|
||||
--deadline=300s \
|
||||
--enable=deadcode \
|
||||
--enable=gochecknoinits \
|
||||
--enable=gofmt \
|
||||
|
|
|
|||
80
api.go
80
api.go
|
|
@ -29,6 +29,7 @@ import (
|
|||
"github.com/pilosa/pilosa/pql"
|
||||
"github.com/pilosa/pilosa/roaring"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// API provides the top level programmatic interface to Pilosa. It is usually
|
||||
|
|
@ -109,8 +110,9 @@ func (api *API) Query(ctx context.Context, req *QueryRequest) (QueryResponse, er
|
|||
}
|
||||
execOpts := &execOptions{
|
||||
Remote: req.Remote,
|
||||
ExcludeRowAttrs: req.ExcludeRowAttrs,
|
||||
ExcludeColumns: req.ExcludeColumns,
|
||||
ExcludeRowAttrs: req.ExcludeRowAttrs, // NOTE: Kept for Pilosa 1.x compat.
|
||||
ExcludeColumns: req.ExcludeColumns, // NOTE: Kept for Pilosa 1.x compat.
|
||||
ColumnAttrs: req.ColumnAttrs, // NOTE: Kept for Pilosa 1.x compat.
|
||||
}
|
||||
results, err := api.server.executor.Execute(ctx, req.Index, q, req.Shards, execOpts)
|
||||
if err != nil {
|
||||
|
|
@ -119,7 +121,8 @@ func (api *API) Query(ctx context.Context, req *QueryRequest) (QueryResponse, er
|
|||
resp.Results = results
|
||||
|
||||
// Fill column attributes if requested.
|
||||
if req.ColumnAttrs && !req.ExcludeColumns {
|
||||
// execOpts.ColumnAttrs may be set by the Execute method if any of the Calls use Options(columnAttrs=true)
|
||||
if execOpts.ColumnAttrs {
|
||||
// Consolidate all column ids across all calls.
|
||||
var columnIDs []uint64
|
||||
for _, result := range results {
|
||||
|
|
@ -294,6 +297,55 @@ func (api *API) Field(_ context.Context, indexName, fieldName string) (*Field, e
|
|||
return field, nil
|
||||
}
|
||||
|
||||
// ImportRoaring is a low level interface for importing data to Pilosa when
|
||||
// extremely high throughput is desired. The data must be encoded in a
|
||||
// particular way which may be unintuitive (discussed below). The data is merged
|
||||
// with existing data.
|
||||
//
|
||||
// It takes as input a roaring bitmap which it uses as the data for the
|
||||
// indicated index, field, and shard. The bitmap may be encoded according to the
|
||||
// official roaring spec (https://github.com/RoaringBitmap/RoaringFormatSpec),
|
||||
// or to the pilosa roaring spec which supports 64 bit integers
|
||||
// (https://www.pilosa.com/docs/latest/architecture/#roaring-bitmap-storage-format).
|
||||
//
|
||||
// The data should be encoded the same way that Pilosa stores fragments
|
||||
// internally. A bit "i" being set in the input bitmap indicates that the bit is
|
||||
// set in Pilosa row "i/ShardWidth", and in column
|
||||
// (shard*ShardWidth)+(i%ShardWidth). That is to say that "data" represents all
|
||||
// of the rows in this shard of this field concatenated together in one long
|
||||
// bitmap.
|
||||
func (api *API) ImportRoaring(ctx context.Context, indexName, fieldName string, shard uint64, remote bool, data []byte) (err error) {
|
||||
if err = api.validate(apiField); err != nil {
|
||||
return errors.Wrap(err, "validating api method")
|
||||
}
|
||||
nodes := api.cluster.shardNodes(indexName, shard)
|
||||
var eg errgroup.Group
|
||||
|
||||
for _, node := range nodes {
|
||||
node := node
|
||||
if node.ID == api.server.nodeID {
|
||||
field := api.holder.Field(indexName, fieldName)
|
||||
if field == nil {
|
||||
return newNotFoundError(ErrFieldNotFound)
|
||||
}
|
||||
// must make a copy of data to operate on locally. field.importRoaring changes data
|
||||
d2 := make([]byte, len(data))
|
||||
copy(d2, data)
|
||||
eg.Go(func() error {
|
||||
return field.importRoaring(d2, shard)
|
||||
})
|
||||
go func(node *Node) {
|
||||
}(node)
|
||||
} else if !remote { // if remote == true we don't forward to other nodes
|
||||
// forward it on
|
||||
eg.Go(func() error {
|
||||
return api.server.defaultClient.ImportRoaring(ctx, &node.URI, indexName, fieldName, shard, true, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
// DeleteField removes the named field from the named index. If the index is not
|
||||
// found, an error is returned. If the field is not found, it is ignored and no
|
||||
// action is taken.
|
||||
|
|
@ -684,6 +736,12 @@ func (api *API) Import(_ context.Context, req *ImportRequest) error {
|
|||
timestamps[i] = &t
|
||||
}
|
||||
|
||||
// Import columnIDs into existence field.
|
||||
if err := importExistenceColumns(index, req.ColumnIDs); err != nil {
|
||||
api.server.logger.Printf("import existence error: index=%s, field=%s, shard=%d, columns=%d, err=%s", req.Index, req.Field, req.Shard, len(req.ColumnIDs), err)
|
||||
return errors.Wrap(err, "importing existence columns")
|
||||
}
|
||||
|
||||
// Import into fragment.
|
||||
err = field.Import(req.RowIDs, req.ColumnIDs, timestamps)
|
||||
if err != nil {
|
||||
|
|
@ -718,6 +776,12 @@ func (api *API) ImportValue(_ context.Context, req *ImportValueRequest) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Import columnIDs into existence field.
|
||||
if err := importExistenceColumns(index, req.ColumnIDs); err != nil {
|
||||
api.server.logger.Printf("import existence error: index=%s, field=%s, shard=%d, columns=%d, err=%s", req.Index, req.Field, req.Shard, len(req.ColumnIDs), err)
|
||||
return errors.Wrap(err, "importing existence columns")
|
||||
}
|
||||
|
||||
// Import into fragment.
|
||||
err = field.importValue(req.ColumnIDs, req.Values)
|
||||
if err != nil {
|
||||
|
|
@ -726,6 +790,16 @@ func (api *API) ImportValue(_ context.Context, req *ImportValueRequest) error {
|
|||
return errors.Wrap(err, "importing")
|
||||
}
|
||||
|
||||
func importExistenceColumns(index *Index, columnIDs []uint64) error {
|
||||
ef := index.existenceField()
|
||||
if ef == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
existenceRowIDs := make([]uint64, len(columnIDs))
|
||||
return ef.Import(existenceRowIDs, columnIDs, nil)
|
||||
}
|
||||
|
||||
// MaxShards returns the maximum shard number for each index in a map.
|
||||
func (api *API) MaxShards(_ context.Context) map[string]uint64 {
|
||||
m := make(map[string]uint64)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
package pilosa
|
||||
|
||||
import "fmt"
|
||||
import "strconv"
|
||||
|
||||
const _apiMethod_name = "apiClusterMessageapiCreateFieldapiCreateIndexapiDeleteFieldapiDeleteIndexapiDeleteViewapiExportCSVapiFragmentBlockDataapiFragmentBlocksapiFieldapiFieldAttrDiffapiImportapiImportValueapiIndexapiIndexAttrDiffapiQueryapiRecalculateCachesapiRemoveNodeapiResizeAbortapiSetCoordinatorapiShardNodesapiViews"
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ var _apiMethod_index = [...]uint16{0, 17, 31, 45, 59, 73, 86, 98, 118, 135, 143,
|
|||
|
||||
func (i apiMethod) String() string {
|
||||
if i < 0 || i >= apiMethod(len(_apiMethod_index)-1) {
|
||||
return fmt.Sprintf("apiMethod(%d)", i)
|
||||
return "apiMethod(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _apiMethod_name[_apiMethod_index[i]:_apiMethod_index[i+1]]
|
||||
}
|
||||
|
|
|
|||
4
cache.go
4
cache.go
|
|
@ -40,10 +40,10 @@ type cache interface {
|
|||
// Returns a list of all IDs.
|
||||
IDs() []uint64
|
||||
|
||||
// Updates the cache, if necessary.
|
||||
// Soft ask for the cache to be rebuilt - may not if it has been done recently.
|
||||
Invalidate()
|
||||
|
||||
// Rebuilds the cache
|
||||
// Rebuilds the cache.
|
||||
Recalculate()
|
||||
|
||||
// Returns an ordered list of the top ranked bitmaps.
|
||||
|
|
|
|||
12
client.go
12
client.go
|
|
@ -41,16 +41,19 @@ type InternalClient interface {
|
|||
ImportK(ctx context.Context, index, field string, bits []Bit) error
|
||||
EnsureIndex(ctx context.Context, name string, options IndexOptions) error
|
||||
EnsureField(ctx context.Context, indexName string, fieldName string) error
|
||||
EnsureFieldWithOptions(ctx context.Context, index, field string, opt FieldOptions) error
|
||||
ImportValue(ctx context.Context, index, field string, shard uint64, vals []FieldValue) error
|
||||
ImportValueK(ctx context.Context, index, field string, vals []FieldValue) error
|
||||
ExportCSV(ctx context.Context, index, field string, shard uint64, w io.Writer) error
|
||||
CreateField(ctx context.Context, index, field string) error
|
||||
CreateFieldWithOptions(ctx context.Context, index, field string, opt FieldOptions) error
|
||||
FragmentBlocks(ctx context.Context, uri *URI, index, field, view string, shard uint64) ([]FragmentBlock, error)
|
||||
BlockData(ctx context.Context, uri *URI, index, field, view string, shard uint64, block int) ([]uint64, []uint64, error)
|
||||
ColumnAttrDiff(ctx context.Context, uri *URI, index string, blks []AttrBlock) (map[uint64]map[string]interface{}, error)
|
||||
RowAttrDiff(ctx context.Context, uri *URI, index, field string, blks []AttrBlock) (map[uint64]map[string]interface{}, error)
|
||||
SendMessage(ctx context.Context, uri *URI, msg []byte) error
|
||||
RetrieveShardFromURI(ctx context.Context, index, field string, shard uint64, uri URI) (io.ReadCloser, error)
|
||||
ImportRoaring(ctx context.Context, uri *URI, index, field string, shard uint64, remote bool, data []byte) error
|
||||
}
|
||||
|
||||
//===============
|
||||
|
|
@ -106,12 +109,18 @@ func (n nopInternalClient) Import(ctx context.Context, index, field string, shar
|
|||
func (n nopInternalClient) ImportK(ctx context.Context, index, field string, bits []Bit) error {
|
||||
return nil
|
||||
}
|
||||
func (n nopInternalClient) ImportRoaring(ctx context.Context, uri *URI, index, field string, shard uint64, remote bool, data []byte) error {
|
||||
return nil
|
||||
}
|
||||
func (n nopInternalClient) EnsureIndex(ctx context.Context, name string, options IndexOptions) error {
|
||||
return nil
|
||||
}
|
||||
func (n nopInternalClient) EnsureField(ctx context.Context, indexName string, fieldName string) error {
|
||||
return nil
|
||||
}
|
||||
func (n nopInternalClient) EnsureFieldWithOptions(ctx context.Context, index, field string, opt FieldOptions) error {
|
||||
return nil
|
||||
}
|
||||
func (n nopInternalClient) ImportValue(ctx context.Context, index, field string, shard uint64, vals []FieldValue) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -122,6 +131,9 @@ func (n nopInternalClient) ExportCSV(ctx context.Context, index, field string, s
|
|||
return nil
|
||||
}
|
||||
func (n nopInternalClient) CreateField(ctx context.Context, index, field string) error { return nil }
|
||||
func (n nopInternalClient) CreateFieldWithOptions(ctx context.Context, index, field string, opt FieldOptions) error {
|
||||
return nil
|
||||
}
|
||||
func (n nopInternalClient) FragmentBlocks(ctx context.Context, uri *URI, index, field, view string, shard uint64) ([]FragmentBlock, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
|
|
@ -27,8 +26,8 @@ import (
|
|||
|
||||
var checker *ctl.CheckCommand
|
||||
|
||||
func newCheckCommand(_ io.Reader, _, _ io.Writer) *cobra.Command {
|
||||
checker = ctl.NewCheckCommand(os.Stdin, os.Stdout, os.Stderr)
|
||||
func newCheckCommand(stdin io.Reader, stdout io.Writer, stderr io.Writer) *cobra.Command {
|
||||
checker = ctl.NewCheckCommand(stdin, stdout, stderr)
|
||||
checkCmd := &cobra.Command{
|
||||
Use: "check <path> [path2]...",
|
||||
Short: "Do a consistency check on a pilosa data file.",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ package cmd
|
|||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
|
|
@ -28,7 +27,7 @@ import (
|
|||
var conf *ctl.ConfigCommand
|
||||
|
||||
func newConfigCommand(stdin io.Reader, stdout, stderr io.Writer) *cobra.Command {
|
||||
conf = ctl.NewConfigCommand(os.Stdin, os.Stdout, os.Stderr)
|
||||
conf = ctl.NewConfigCommand(stdin, stdout, stderr)
|
||||
Server := server.NewCommand(stdin, stdout, stderr)
|
||||
confCmd := &cobra.Command{
|
||||
Use: "config",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ package cmd
|
|||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
|
|
@ -26,8 +25,8 @@ import (
|
|||
|
||||
var Exporter *ctl.ExportCommand
|
||||
|
||||
func newExportCommand(_ io.Reader, _, _ io.Writer) *cobra.Command {
|
||||
Exporter = ctl.NewExportCommand(os.Stdin, os.Stdout, os.Stderr)
|
||||
func newExportCommand(stdin io.Reader, stdout, stderr io.Writer) *cobra.Command {
|
||||
Exporter = ctl.NewExportCommand(stdin, stdout, stderr)
|
||||
exportCmd := &cobra.Command{
|
||||
Use: "export",
|
||||
Short: "Export data from pilosa.",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ package cmd
|
|||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
|
|
@ -26,8 +25,8 @@ import (
|
|||
|
||||
var generateConf *ctl.GenerateConfigCommand
|
||||
|
||||
func newGenerateConfigCommand(_ io.Reader, _, _ io.Writer) *cobra.Command {
|
||||
generateConf = ctl.NewGenerateConfigCommand(os.Stdin, os.Stdout, os.Stderr)
|
||||
func newGenerateConfigCommand(stdin io.Reader, stdout io.Writer, stderr io.Writer) *cobra.Command {
|
||||
generateConf = ctl.NewGenerateConfigCommand(stdin, stdout, stderr)
|
||||
confCmd := &cobra.Command{
|
||||
Use: "generate-config",
|
||||
Short: "Print the default configuration.",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import (
|
|||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/pilosa/pilosa"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/pilosa/pilosa/ctl"
|
||||
|
|
@ -51,13 +53,16 @@ omitted. If it is present then its format should be YYYY-MM-DDTHH:MM.
|
|||
flags.StringVarP(&Importer.Host, "host", "", "localhost:10101", "host:port of Pilosa.")
|
||||
flags.StringVarP(&Importer.Index, "index", "i", "", "Pilosa index to import into.")
|
||||
flags.StringVarP(&Importer.Field, "field", "f", "", "Field to import into.")
|
||||
flags.BoolVar(&Importer.StringKeys, "string-keys", false, "REMOVED (key type is now determined by index/field configuration): Treat payload as string keys.")
|
||||
flags.BoolVar(&Importer.IndexOptions.Keys, "index-keys", false, "Specify keys=true when creating an index")
|
||||
flags.BoolVar(&Importer.FieldOptions.Keys, "field-keys", false, "Specify keys=true when creating a field")
|
||||
flags.Int64Var(&Importer.FieldOptions.Min, "field-min", 0, "Specify the minimum for an int field on creation")
|
||||
flags.Int64Var(&Importer.FieldOptions.Max, "field-max", 0, "Specify the maximum for an int field on creation")
|
||||
flags.StringVar(&Importer.FieldOptions.CacheType, "field-cache-type", pilosa.CacheTypeRanked, "Specify the cache type for a set field on creation. One of: none, lru, ranked")
|
||||
flags.Uint32Var(&Importer.FieldOptions.CacheSize, "field-cache-size", 50000, "Specify the cache size for a set field on creation")
|
||||
flags.Var(&Importer.FieldOptions.TimeQuantum, "field-time-quantum", "Specify the time quantum for a time field on creation. One of: D, DH, H, M, MD, MDH, Y, YM, YMD, YMDH")
|
||||
flags.IntVarP(&Importer.BufferSize, "buffer-size", "s", 10000000, "Number of bits to buffer/sort before importing.")
|
||||
flags.BoolVarP(&Importer.Sort, "sort", "", false, "Enables sorting before import.")
|
||||
flags.BoolVarP(&Importer.CreateSchema, "create", "e", false, "Create the schema if it does not exist before import.")
|
||||
//flags.Var(&Importer.FieldOptions.TimeQuantum, "field-time-quantum", "Time quantum for the field")
|
||||
//flags.StringVar(&Importer.FieldOptions.CacheType, "field-cache-type", pilosa.CacheTypeRanked, "Cache type for the field; valid values: none, lru, ranked")
|
||||
//flags.Uint32Var(&Importer.FieldOptions.CacheSize, "field-cache-size", 50000, "Cache size for the field")
|
||||
ctl.SetTLSConfig(flags, &Importer.TLS.CertificatePath, &Importer.TLS.CertificateKeyPath, &Importer.TLS.SkipVerify)
|
||||
|
||||
return importCmd
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pilosa/pilosa"
|
||||
|
||||
"github.com/pilosa/pilosa/cmd"
|
||||
)
|
||||
|
||||
|
|
@ -47,6 +49,52 @@ field = "f1"
|
|||
return v.Error()
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"import", "--index", "i1", "--field", "f1", "--field-keys", "--field-min", "-10", "--field-max", "100"},
|
||||
env: map[string]string{},
|
||||
validation: func() error {
|
||||
v := validator{}
|
||||
v.Check(cmd.Importer.Index, "i1")
|
||||
v.Check(cmd.Importer.Field, "f1")
|
||||
v.Check(cmd.Importer.FieldOptions, pilosa.FieldOptions{
|
||||
Keys: true,
|
||||
Max: 100,
|
||||
Min: -10,
|
||||
CacheType: pilosa.CacheTypeRanked,
|
||||
CacheSize: 50000,
|
||||
})
|
||||
return v.Error()
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"import", "--index", "i1", "--field", "f1", "--field-time-quantum", "YMD"},
|
||||
env: map[string]string{},
|
||||
validation: func() error {
|
||||
v := validator{}
|
||||
v.Check(cmd.Importer.Index, "i1")
|
||||
v.Check(cmd.Importer.Field, "f1")
|
||||
v.Check(cmd.Importer.FieldOptions, pilosa.FieldOptions{
|
||||
TimeQuantum: "YMD",
|
||||
CacheType: pilosa.CacheTypeRanked,
|
||||
CacheSize: 50000,
|
||||
})
|
||||
return v.Error()
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"import", "--index", "i1", "--field", "f1", "--field-cache-type", "lru", "--field-cache-size", "100"},
|
||||
env: map[string]string{},
|
||||
validation: func() error {
|
||||
v := validator{}
|
||||
v.Check(cmd.Importer.Index, "i1")
|
||||
v.Check(cmd.Importer.Field, "f1")
|
||||
v.Check(cmd.Importer.FieldOptions, pilosa.FieldOptions{
|
||||
CacheType: "lru",
|
||||
CacheSize: 100,
|
||||
})
|
||||
return v.Error()
|
||||
},
|
||||
},
|
||||
}
|
||||
executeDry(t, tests)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
|
|
@ -27,8 +26,8 @@ import (
|
|||
|
||||
var inspector *ctl.InspectCommand
|
||||
|
||||
func newInspectCommand(_ io.Reader, _, _ io.Writer) *cobra.Command {
|
||||
inspector = ctl.NewInspectCommand(os.Stdin, os.Stdout, os.Stderr)
|
||||
func newInspectCommand(stdin io.Reader, stdout, stderr io.Writer) *cobra.Command {
|
||||
inspector = ctl.NewInspectCommand(stdin, stdout, stderr)
|
||||
|
||||
inspectCmd := &cobra.Command{
|
||||
Use: "inspect",
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ func TestCheckCommand_Run(t *testing.T) {
|
|||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
|
||||
if !strings.HasPrefix(err.Error(), "checking bitmap: unmarshalling: invalid roaring file") {
|
||||
if !strings.HasPrefix(err.Error(), "checking bitmap: unmarshalling: reading roaring header:") {
|
||||
t.Fatalf("expect error: invalid roaring file, actual: '%s'", err)
|
||||
}
|
||||
// Todo: need correct roaring file for happy path
|
||||
|
|
|
|||
|
|
@ -40,16 +40,15 @@ type ImportCommand struct { // nolint: maligned
|
|||
Index string `json:"index"`
|
||||
Field string `json:"field"`
|
||||
|
||||
// Options for index & field to be created if they don't exist
|
||||
indexOptions pilosa.IndexOptions
|
||||
// Options for the index to be created if it doesn't exist
|
||||
IndexOptions pilosa.IndexOptions
|
||||
|
||||
// Options for the field to be created if it doesn't exist
|
||||
FieldOptions pilosa.FieldOptions
|
||||
|
||||
// CreateSchema ensures the schema exists before import
|
||||
CreateSchema bool
|
||||
|
||||
// REMOVED: Indicates that the payload should be treated as string keys.
|
||||
// TODO: remove this in a future release
|
||||
StringKeys bool `json:"StringKeys"`
|
||||
|
||||
// Filenames to import from.
|
||||
Paths []string `json:"paths"`
|
||||
|
||||
|
|
@ -80,11 +79,6 @@ func NewImportCommand(stdin io.Reader, stdout, stderr io.Writer) *ImportCommand
|
|||
func (cmd *ImportCommand) Run(ctx context.Context) error {
|
||||
logger := log.New(cmd.Stderr, "", log.LstdFlags)
|
||||
|
||||
// REMOVED: warning that --string-keys flag has been deprecated.
|
||||
if cmd.StringKeys {
|
||||
logger.Printf("REMOVED: The string-keys flag is no longer used.")
|
||||
}
|
||||
|
||||
// Validate arguments.
|
||||
// Index and field are validated early before the files are parsed.
|
||||
if cmd.Index == "" {
|
||||
|
|
@ -102,6 +96,14 @@ func (cmd *ImportCommand) Run(ctx context.Context) error {
|
|||
cmd.client = client
|
||||
|
||||
if cmd.CreateSchema {
|
||||
// set the correct type for the field
|
||||
if cmd.FieldOptions.TimeQuantum != "" {
|
||||
cmd.FieldOptions.Type = "time"
|
||||
} else if cmd.FieldOptions.Min != 0 || cmd.FieldOptions.Max != 0 {
|
||||
cmd.FieldOptions.Type = "int"
|
||||
} else {
|
||||
cmd.FieldOptions.Type = "set"
|
||||
}
|
||||
err := cmd.ensureSchema(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "ensuring schema")
|
||||
|
|
@ -142,11 +144,11 @@ func (cmd *ImportCommand) Run(ctx context.Context) error {
|
|||
}
|
||||
|
||||
func (cmd *ImportCommand) ensureSchema(ctx context.Context) error {
|
||||
err := cmd.client.EnsureIndex(ctx, cmd.Index, cmd.indexOptions)
|
||||
err := cmd.client.EnsureIndex(ctx, cmd.Index, cmd.IndexOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error Creating Index: %s", err)
|
||||
}
|
||||
err = cmd.client.EnsureField(ctx, cmd.Index, cmd.Field)
|
||||
err = cmd.client.EnsureFieldWithOptions(ctx, cmd.Index, cmd.Field, cmd.FieldOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error Creating Field: %s", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -294,6 +294,8 @@ We currently track the following events
|
|||
- **Union:** Count of Union queries.
|
||||
- **Intersection:** Count of Intersection queries.
|
||||
- **Difference:** Count of Difference queries.
|
||||
- **Xor:** Count of Xor queries.
|
||||
- **Not:** Count of Not queries.
|
||||
- **Count:** Count of Count queries.
|
||||
- **Range:** Count of Range queries.
|
||||
- **Snapshot:** Event count when the snapshot process is triggered.
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ func main() {
|
|||
}
|
||||
|
||||
// We need to refer to indexes and fields before we can use them in a query.
|
||||
repository, _ := schema.Index("repository")
|
||||
stargazer, _ := repository.Field("stargazer")
|
||||
language, _ := repository.Field("language")
|
||||
repository := schema.Index("repository")
|
||||
stargazer := repository.Field("stargazer")
|
||||
language := repository.Field("language")
|
||||
|
||||
var response *pilosa.QueryResponse
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,11 @@ curl localhost:10101/index/repository/query \
|
|||
* `field` The field specifies on which Pilosa [field](../glossary/#field) the query will operate. Valid field names are lower case strings; they start with an alphanumeric character, and contain only alphanumeric characters and `_-`. They must be 64 characters or less in length.
|
||||
* `TIMESTAMP` This is a timestamp in the following format `YYYY-MM-DDTHH:MM` (e.g. 2006-01-02T15:04)
|
||||
* `UINT` An unsigned integer (e.g. 42839)
|
||||
* `BOOL` A boolean value, `true` or `false`
|
||||
* `ATTR_NAME` Must be a valid identifier `[A-Za-z][A-Za-z0-9._-]*`
|
||||
* `ATTR_VALUE` Can be a string, float, integer, or bool.
|
||||
* `ROW_CALL` Any query which returns a row, such as `Row`, `Union`, `Difference`, `Xor`, `Intersect`, `Range`
|
||||
* `CALL` Any query
|
||||
* `ROW_CALL` Any query which returns a row, such as `Row`, `Union`, `Difference`, `Xor`, `Intersect`, `Range`, `Not`
|
||||
* `[]ATTR_VALUE` Denotes an array of `ATTR_VALUE`s. (e.g. `["a", "b", "c"]`)
|
||||
|
||||
### Write Operations
|
||||
|
|
@ -94,7 +96,7 @@ Set(10, stargazer=1, 2016-01-01T00:00)
|
|||
|
||||
Set multiple bits in a single request:
|
||||
```request
|
||||
Set(1, stargazer=10) Set(2, stargazer=10) Set(1, stargazer=20) Set(2, stargazer=30)
|
||||
Set(10, stargazer=1) Set(20, stargazer=1) Set(10, stargazer=2) Set(30, stargazer=2)
|
||||
```
|
||||
```response
|
||||
{"results":[false,true,true,true]}
|
||||
|
|
@ -112,8 +114,8 @@ Set(10, pullrequests=2)
|
|||
**Spec:**
|
||||
|
||||
```
|
||||
SetRowAttrs(<FIELD>, <ROW>,
|
||||
<ATTR_NAME=ATTR_VALUE>,
|
||||
SetRowAttrs(<FIELD>, <ROW>,
|
||||
<ATTR_NAME=ATTR_VALUE>,
|
||||
[ATTR_NAME=ATTR_VALUE ...])
|
||||
```
|
||||
|
||||
|
|
@ -150,8 +152,8 @@ SetRowAttrs(stargazer, 10, username=null)
|
|||
**Spec:**
|
||||
|
||||
```
|
||||
SetColumnAttrs(<COLUMN>,
|
||||
<ATTR_NAME=ATTR_VALUE>,
|
||||
SetColumnAttrs(<COLUMN>,
|
||||
<ATTR_NAME=ATTR_VALUE>,
|
||||
[ATTR_NAME=ATTR_VALUE ...])
|
||||
```
|
||||
|
||||
|
|
@ -262,7 +264,7 @@ Row(stargazer=1)
|
|||
{"attrs":{"username":"mrpi","active":true},"columns":[10, 20]}
|
||||
```
|
||||
|
||||
* attrs are the attributes for user 1
|
||||
* attrs are the attributes for user 1
|
||||
* columns are the repositories which user 1 has starred.
|
||||
|
||||
#### Union
|
||||
|
|
@ -358,7 +360,7 @@ Difference(Row(stargazer=2), Row(stargazer=1))
|
|||
{"attrs":{},"columns":[30]}
|
||||
```
|
||||
|
||||
* columnss are repositories that were starred by user 2 BUT NOT user 1
|
||||
* columns are repositories that were starred by user 2 BUT NOT user 1
|
||||
|
||||
#### Xor
|
||||
|
||||
|
|
@ -384,11 +386,39 @@ Query columns with a bit set in exactly one of two rows (repositories that are s
|
|||
Xor(Row(stargazer=2), Row(stargazer=1))
|
||||
```
|
||||
```response
|
||||
{"results":[{"attrs":{},"columns":[10,20,30]}]}
|
||||
{"results":[{"attrs":{},"columns":[20,30]}]}
|
||||
```
|
||||
|
||||
* columns are repositories that were starred by user 1 XOR user 2 (user 1 or user 2, but not both)
|
||||
|
||||
#### Not
|
||||
|
||||
**Spec:**
|
||||
|
||||
```
|
||||
Not(<ROW_CALL>)
|
||||
```
|
||||
|
||||
**Description:**
|
||||
|
||||
Not returns the inverse of all of the bits from the `ROW_CALL` argument. The Not query requires that `trackExistence` has been enabled on the Index.
|
||||
|
||||
**Result Type:** object with attrs and columns
|
||||
|
||||
attrs will always be empty
|
||||
|
||||
**Examples:**
|
||||
|
||||
Query existing columns that do not have a bit set in the given row.
|
||||
```request
|
||||
Not(Row(stargazer=1))
|
||||
```
|
||||
```response
|
||||
{"results":[{"attrs":{},"columns":[30]}]}
|
||||
```
|
||||
|
||||
* columns are repositories that were not starred by user 1
|
||||
|
||||
#### Count
|
||||
**Spec:**
|
||||
|
||||
|
|
@ -497,7 +527,7 @@ Range(<FIELD>=<ROW>, <TIMESTAMP>, <TIMESTAMP>)
|
|||
**Description:**
|
||||
|
||||
Similar to `Row`, but only returns bits which were set with timestamps
|
||||
between the given `start` (first) and `end` (second) timestamps.
|
||||
between the given `start` (first) and `end` (second) timestamps.
|
||||
|
||||
**Result Type:** object with attrs and bits
|
||||
|
||||
|
|
@ -548,14 +578,14 @@ Range(commitactivity > 100)
|
|||
|
||||
BSI range queries support the following operators:
|
||||
|
||||
Operator | Name | Value
|
||||
Operator | Name | Value
|
||||
----------|-------------------------------|--------------------
|
||||
`>` | greater-than, GT | integer
|
||||
`<` | less-than, LT | integer
|
||||
`<=` | less-than-or-equal-to, LTE | integer
|
||||
`>=` | greater-than-or-equal-to, GTE | integer
|
||||
`==` | equal-to, EQ | integer
|
||||
`!=` | not-equal-to, NEQ | integer or `null`
|
||||
`>` | greater-than, GT | integer
|
||||
`<` | less-than, LT | integer
|
||||
`<=` | less-than-or-equal-to, LTE | integer
|
||||
`>=` | greater-than-or-equal-to, GTE | integer
|
||||
`==` | equal-to, EQ | integer
|
||||
`!=` | not-equal-to, NEQ | integer or `null`
|
||||
|
||||
`<`, and `<=` can be chained together to represent a bounded interval. For example:
|
||||
|
||||
|
|
@ -645,3 +675,41 @@ Sum(field="diskusage")
|
|||
```
|
||||
|
||||
* Result is the sum of all values (total size of all repositories in kilobytes, here), plus the count of columns.
|
||||
|
||||
### Other Operations
|
||||
|
||||
#### Options
|
||||
|
||||
**Spec:**
|
||||
|
||||
```
|
||||
Options(<CALL>, columnAttrs=<BOOL>, excludeColumns=<BOOL>, excludeRowAttrs=<BOOL>, shards=[UINT ...])
|
||||
```
|
||||
|
||||
**Description:**
|
||||
|
||||
Modifies the given query as follows:
|
||||
* `columnAttrs`: Include column attributes in the result (Default: `false`).
|
||||
* `excludeColumns`: Exclude column IDs from the result (Default: `false`).
|
||||
* `excludeRowAttrs`: Exclude row attributes from the result (Default: `false`).
|
||||
* `shards`: Run the query using only the data from the given shards. By default, the entire data set (i.e. data from all shards) is used.
|
||||
|
||||
**Result Type:** Same result type as <CALL>.
|
||||
|
||||
**Examples:**
|
||||
|
||||
Return column attributes:
|
||||
```request
|
||||
Options(Row(f1=10), columnAttrs=true)
|
||||
```
|
||||
```response
|
||||
{"attrs":{},"columns":[100]}],"columnAttrs":[{"id":100,"attrs":{"foo":"bar"}}
|
||||
```
|
||||
|
||||
Run the query against shards 0 and 2 only:
|
||||
```request
|
||||
Options(Row(f1=10), shards=[0, 2])
|
||||
```
|
||||
```response
|
||||
{"attrs":{},"columns":[100, 2097152]}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -509,7 +509,8 @@ func encodeCreateIndexMessage(m *pilosa.CreateIndexMessage) *internal.CreateInde
|
|||
|
||||
func encodeIndexMeta(m *pilosa.IndexOptions) *internal.IndexMeta {
|
||||
return &internal.IndexMeta{
|
||||
Keys: m.Keys,
|
||||
Keys: m.Keys,
|
||||
TrackExistence: m.TrackExistence,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -741,6 +742,7 @@ func decodeCreateIndexMessage(pb *internal.CreateIndexMessage, m *pilosa.CreateI
|
|||
|
||||
func decodeIndexMeta(pb *internal.IndexMeta, m *pilosa.IndexOptions) {
|
||||
m.Keys = pb.Keys
|
||||
m.TrackExistence = pb.TrackExistence
|
||||
}
|
||||
|
||||
func decodeDeleteIndexMessage(pb *internal.DeleteIndexMessage, m *pilosa.DeleteIndexMessage) {
|
||||
|
|
|
|||
86
executor.go
86
executor.go
|
|
@ -194,6 +194,8 @@ func (e *executor) executeCall(ctx context.Context, index string, c *pql.Call, s
|
|||
case "TopN":
|
||||
e.Holder.Stats.CountWithCustomTags(c.Name, 1, 1.0, []string{indexTag})
|
||||
return e.executeTopN(ctx, index, c, shards, opt)
|
||||
case "Options":
|
||||
return e.executeOptionsCall(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)
|
||||
|
|
@ -219,6 +221,48 @@ func (e *executor) validateCallArgs(c *pql.Call) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (e *executor) executeOptionsCall(ctx context.Context, index string, c *pql.Call, shards []uint64, opt *execOptions) (interface{}, error) {
|
||||
optCopy := &execOptions{}
|
||||
*optCopy = *opt
|
||||
if arg, ok := c.Args["columnAttrs"]; ok {
|
||||
if value, ok := arg.(bool); ok {
|
||||
opt.ColumnAttrs = value
|
||||
} else {
|
||||
return nil, errors.New("Query(): columnAttrs must be a bool")
|
||||
}
|
||||
}
|
||||
if arg, ok := c.Args["excludeRowAttrs"]; ok {
|
||||
if value, ok := arg.(bool); ok {
|
||||
optCopy.ExcludeRowAttrs = value
|
||||
} else {
|
||||
return nil, errors.New("Query(): excludeRowAttrs must be a bool")
|
||||
}
|
||||
}
|
||||
if arg, ok := c.Args["excludeColumns"]; ok {
|
||||
if value, ok := arg.(bool); ok {
|
||||
optCopy.ExcludeColumns = value
|
||||
} else {
|
||||
return nil, errors.New("Query(): excludeColumns must be a bool")
|
||||
}
|
||||
}
|
||||
if arg, ok := c.Args["shards"]; ok {
|
||||
if optShards, ok := arg.([]interface{}); ok {
|
||||
shards = []uint64{}
|
||||
for _, s := range optShards {
|
||||
if shard, ok := s.(int64); ok {
|
||||
shards = append(shards, uint64(shard))
|
||||
} else {
|
||||
return nil, errors.New("Query(): shards must be a list of unsigned integers")
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("Query(): shards must be a list of unsigned integers")
|
||||
}
|
||||
}
|
||||
return e.executeCall(ctx, index, c.Children[0], shards, optCopy)
|
||||
}
|
||||
|
||||
// executeSum executes a Sum() call.
|
||||
func (e *executor) executeSum(ctx context.Context, index string, c *pql.Call, shards []uint64, opt *execOptions) (ValCount, error) {
|
||||
if field := c.Args["field"]; field == "" {
|
||||
|
|
@ -399,6 +443,8 @@ func (e *executor) executeBitmapCallShard(ctx context.Context, index string, c *
|
|||
return e.executeUnionShard(ctx, index, c, shard)
|
||||
case "Xor":
|
||||
return e.executeXorShard(ctx, index, c, shard)
|
||||
case "Not":
|
||||
return e.executeNotShard(ctx, index, c, shard)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown call: %s", c.Name)
|
||||
}
|
||||
|
|
@ -963,6 +1009,38 @@ func (e *executor) executeXorShard(ctx context.Context, index string, c *pql.Cal
|
|||
return other, nil
|
||||
}
|
||||
|
||||
// executeNotShard executes a not() call for a local shard.
|
||||
func (e *executor) executeNotShard(ctx context.Context, index string, c *pql.Call, shard uint64) (*Row, error) {
|
||||
if len(c.Children) == 0 {
|
||||
return nil, errors.New("Not() requires an input row")
|
||||
} else if len(c.Children) > 1 {
|
||||
return nil, errors.New("Not() only accepts a single row input")
|
||||
}
|
||||
|
||||
// Make sure the index supports existence tracking.
|
||||
idx := e.Holder.Index(index)
|
||||
if idx == nil {
|
||||
return nil, ErrIndexNotFound
|
||||
} else if idx.existenceField() == nil {
|
||||
return nil, errors.Errorf("index does not support existence tracking: %s", index)
|
||||
}
|
||||
|
||||
var existenceRow *Row
|
||||
existenceFrag := e.Holder.fragment(index, existenceFieldName, viewStandard, shard)
|
||||
if existenceFrag == nil {
|
||||
existenceRow = NewRow()
|
||||
} else {
|
||||
existenceRow = existenceFrag.row(0)
|
||||
}
|
||||
|
||||
row, err := e.executeBitmapCallShard(ctx, index, c.Children[0], shard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return existenceRow.Difference(row), nil
|
||||
}
|
||||
|
||||
// executeCount executes a count() call.
|
||||
func (e *executor) executeCount(ctx context.Context, index string, c *pql.Call, shards []uint64, opt *execOptions) (uint64, error) {
|
||||
if len(c.Children) == 0 {
|
||||
|
|
@ -1087,6 +1165,13 @@ func (e *executor) executeSet(ctx context.Context, index string, c *pql.Call, op
|
|||
return false, ErrFieldNotFound
|
||||
}
|
||||
|
||||
// Set column on existence field.
|
||||
if ef := idx.existenceField(); ef != nil {
|
||||
if _, err := ef.SetBit(0, colID, nil); err != nil {
|
||||
return false, errors.Wrap(err, "setting existence column")
|
||||
}
|
||||
}
|
||||
|
||||
if f.Type() == FieldTypeInt {
|
||||
// Read remaining fields using labels.
|
||||
rowVal, ok, err := c.IntArg(fieldName)
|
||||
|
|
@ -1677,6 +1762,7 @@ type execOptions struct {
|
|||
Remote bool
|
||||
ExcludeRowAttrs bool
|
||||
ExcludeColumns bool
|
||||
ColumnAttrs bool
|
||||
}
|
||||
|
||||
// hasOnlySetRowAttrs returns true if calls only contains SetRowAttrs() calls.
|
||||
|
|
|
|||
292
executor_test.go
292
executor_test.go
|
|
@ -17,6 +17,7 @@ package pilosa_test
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -32,8 +33,8 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Ensure a bitmap query can be executed.
|
||||
func TestExecutor_Execute_Bitmap(t *testing.T) {
|
||||
// Ensure a row query can be executed.
|
||||
func TestExecutor_Execute_Row(t *testing.T) {
|
||||
t.Run("Row", func(t *testing.T) {
|
||||
c := test.MustRunCluster(t, 1)
|
||||
defer c.Close()
|
||||
|
|
@ -282,7 +283,7 @@ func TestExecutor_Execute_SetBit(t *testing.T) {
|
|||
t.Run("OK", func(t *testing.T) {
|
||||
hldr.ClearBit("i", "f", 11, 1)
|
||||
if n := hldr.Row("i", "f", 11).Count(); n != 0 {
|
||||
t.Fatalf("unexpected bitmap count: %d", n)
|
||||
t.Fatalf("unexpected row count: %d", n)
|
||||
}
|
||||
|
||||
if res, err := cmd.API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Set(1, f=11)`}); err != nil {
|
||||
|
|
@ -294,7 +295,7 @@ func TestExecutor_Execute_SetBit(t *testing.T) {
|
|||
}
|
||||
|
||||
if n := hldr.Row("i", "f", 11).Count(); n != 1 {
|
||||
t.Fatalf("unexpected bitmap count: %d", n)
|
||||
t.Fatalf("unexpected row count: %d", n)
|
||||
}
|
||||
if res, err := cmd.API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Set(1, f=11)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -327,7 +328,7 @@ func TestExecutor_Execute_SetBit(t *testing.T) {
|
|||
t.Run("OK", func(t *testing.T) {
|
||||
hldr.SetBit("i", "f", 1, 0)
|
||||
if n := hldr.Row("i", "f", 11).Count(); n != 0 {
|
||||
t.Fatalf("unexpected bitmap count: %d", n)
|
||||
t.Fatalf("unexpected row count: %d", n)
|
||||
}
|
||||
|
||||
if res, err := cmd.API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Set("foo", f=11)`}); err != nil {
|
||||
|
|
@ -339,7 +340,7 @@ func TestExecutor_Execute_SetBit(t *testing.T) {
|
|||
}
|
||||
|
||||
if n := hldr.Row("i", "f", 11).Count(); n != 1 {
|
||||
t.Fatalf("unexpected bitmap count: %d", n)
|
||||
t.Fatalf("unexpected row count: %d", n)
|
||||
}
|
||||
if res, err := cmd.API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Set("foo", f=11)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -476,7 +477,7 @@ func TestExecutor_Execute_SetRowAttrs(t *testing.T) {
|
|||
}
|
||||
t.Run("rowID", func(t *testing.T) {
|
||||
// Set two attrs on f/10.
|
||||
// Also set attrs on other bitmaps and fields to test isolation.
|
||||
// Also set attrs on other rows and fields to test isolation.
|
||||
if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `SetRowAttrs(f, 10, foo="bar")`}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -494,13 +495,13 @@ func TestExecutor_Execute_SetRowAttrs(t *testing.T) {
|
|||
if m, err := f.RowAttrStore().Attrs(10); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !reflect.DeepEqual(m, map[string]interface{}{"foo": "bar", "baz": int64(123), "bat": true}) {
|
||||
t.Fatalf("unexpected bitmap attr: %#v", m)
|
||||
t.Fatalf("unexpected row attr: %#v", m)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rowKey", func(t *testing.T) {
|
||||
// Set two attrs on f/10.
|
||||
// Also set attrs on other bitmaps and fields to test isolation.
|
||||
// Also set attrs on other rows and fields to test isolation.
|
||||
if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `SetRowAttrs(kf, "row10", foo="bar")`}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -663,7 +664,7 @@ func TestExecutor_Execute_TopN_fill_small(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure a TopN() query with a source bitmap can be executed.
|
||||
// Ensure a TopN() query with a source row can be executed.
|
||||
func TestExecutor_Execute_TopN_Src(t *testing.T) {
|
||||
c := test.MustRunCluster(t, 1)
|
||||
defer c.Close()
|
||||
|
|
@ -723,7 +724,7 @@ func TestExecutor_Execute_TopN_Attr(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
//Ensure TopN handles Attribute filters with source bitmap
|
||||
//Ensure TopN handles Attribute filters with source row
|
||||
func TestExecutor_Execute_TopN_Attr_Src(t *testing.T) {
|
||||
c := test.MustRunCluster(t, 1)
|
||||
defer c.Close()
|
||||
|
|
@ -1352,3 +1353,272 @@ func TestExecutor_Time_Clear_Quantums(t *testing.T) {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExecutor_QueryCall(t *testing.T) {
|
||||
t.Run("excludeRowAttrs", func(t *testing.T) {
|
||||
c := test.MustRunCluster(t, 1)
|
||||
defer c.Close()
|
||||
hldr := test.Holder{Holder: c[0].Server.Holder()}
|
||||
|
||||
// Set columns for rows 0, 10, & 20 across two shards.
|
||||
if idx, err := hldr.CreateIndex("i", pilosa.IndexOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := idx.CreateField("f", pilosa.OptFieldTypeDefault()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `
|
||||
Set(100, f=10)
|
||||
SetRowAttrs(f, 10, foo="bar")
|
||||
`}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Options(Row(f=10), excludeRowAttrs=true)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{100}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
} else if attrs := res.Results[0].(*pilosa.Row).Attrs; !reflect.DeepEqual(attrs, map[string]interface{}{}) {
|
||||
t.Fatalf("unexpected attrs: %s", spew.Sdump(attrs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("excludeColumns", func(t *testing.T) {
|
||||
c := test.MustRunCluster(t, 1)
|
||||
defer c.Close()
|
||||
hldr := test.Holder{Holder: c[0].Server.Holder()}
|
||||
|
||||
// Set columns for rows 0, 10, & 20 across two shards.
|
||||
if idx, err := hldr.CreateIndex("i", pilosa.IndexOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := idx.CreateField("f", pilosa.OptFieldTypeDefault()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `
|
||||
Set(100, f=10)
|
||||
SetRowAttrs(f, 10, foo="bar")
|
||||
`}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Options(Row(f=10), excludeColumns=true)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
} else if attrs := res.Results[0].(*pilosa.Row).Attrs; !reflect.DeepEqual(attrs, map[string]interface{}{"foo": "bar"}) {
|
||||
t.Fatalf("unexpected attrs: %s", spew.Sdump(attrs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("columnAttrs", func(t *testing.T) {
|
||||
c := test.MustRunCluster(t, 1)
|
||||
defer c.Close()
|
||||
hldr := test.Holder{Holder: c[0].Server.Holder()}
|
||||
|
||||
// Set columns for rows 0, 10, & 20 across two shards.
|
||||
if idx, err := hldr.CreateIndex("i", pilosa.IndexOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := idx.CreateField("f", pilosa.OptFieldTypeDefault()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `
|
||||
Set(100, f=10)
|
||||
SetColumnAttrs(100, foo="bar")
|
||||
`}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
targetColAttrSets := []*pilosa.ColumnAttrSet{
|
||||
{ID: 100, Attrs: map[string]interface{}{"foo": "bar"}},
|
||||
}
|
||||
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Options(Row(f=10), columnAttrs=true)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{100}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
} else if attrs := res.ColumnAttrSets; !reflect.DeepEqual(attrs, targetColAttrSets) {
|
||||
t.Fatalf("unexpected attrs: %s", spew.Sdump(attrs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("shards", func(t *testing.T) {
|
||||
c := test.MustRunCluster(t, 1)
|
||||
defer c.Close()
|
||||
hldr := test.Holder{Holder: c[0].Server.Holder()}
|
||||
|
||||
// Set columns for rows 0, 10, & 20 across two shards.
|
||||
if idx, err := hldr.CreateIndex("i", pilosa.IndexOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := idx.CreateField("f", pilosa.OptFieldTypeDefault()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: fmt.Sprintf(`
|
||||
Set(100, f=10)
|
||||
Set(%d, f=10)
|
||||
Set(%d, f=10)
|
||||
`, ShardWidth, ShardWidth*2)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Options(Row(f=10), shards=[0, 2])`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{100, ShardWidth * 2}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multipleOpt", func(t *testing.T) {
|
||||
c := test.MustRunCluster(t, 1)
|
||||
defer c.Close()
|
||||
hldr := test.Holder{Holder: c[0].Server.Holder()}
|
||||
|
||||
// Set columns for rows 0, 10, & 20 across two shards.
|
||||
if idx, err := hldr.CreateIndex("i", pilosa.IndexOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := idx.CreateField("f", pilosa.OptFieldTypeDefault()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `
|
||||
Set(100, f=10)
|
||||
SetRowAttrs(f, 10, foo="bar")
|
||||
`}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &pilosa.QueryRequest{
|
||||
Index: "i",
|
||||
Query: `Options(Row(f=10), excludeColumns=true)Options(Row(f=10), excludeRowAttrs=true)`,
|
||||
}
|
||||
if res, err := c[0].API.Query(context.Background(), req); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
} else if attrs := res.Results[0].(*pilosa.Row).Attrs; !reflect.DeepEqual(attrs, map[string]interface{}{"foo": "bar"}) {
|
||||
t.Fatalf("unexpected attrs: %s", spew.Sdump(attrs))
|
||||
} else if bits := res.Results[1].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{100}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
} else if attrs := res.Results[1].(*pilosa.Row).Attrs; !reflect.DeepEqual(attrs, map[string]interface{}{}) {
|
||||
t.Fatalf("unexpected attrs: %s", spew.Sdump(attrs))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure an existence field is maintained.
|
||||
func TestExecutor_Execute_Existence(t *testing.T) {
|
||||
t.Run("Row", 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+2, 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)
|
||||
}
|
||||
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Row(exists=0)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{3, ShardWidth + 1, ShardWidth + 2}) {
|
||||
t.Fatalf("unexpected existence columns: %+v", bits)
|
||||
}
|
||||
|
||||
// Reopen cluster to ensure existence field is reloaded.
|
||||
if err := c[0].Reopen(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Row(exists=0)`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{3, ShardWidth + 1, ShardWidth + 2}) {
|
||||
t.Fatalf("unexpected existence columns after reopen: %+v", bits)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure a not query can be executed.
|
||||
func TestExecutor_Execute_Not(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+2, 20),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Populated row.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Not(Row(f=20))`}); 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)
|
||||
}
|
||||
|
||||
// Populated row.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Not(Row(f=0))`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{3, ShardWidth + 1, ShardWidth + 2}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
}
|
||||
|
||||
// All existing.
|
||||
if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Not(Union(Row(f=10), Row(f=20)))`}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{}) {
|
||||
t.Fatalf("unexpected columns: %+v", bits)
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkExistence(nn bool, b *testing.B) {
|
||||
c := test.MustRunCluster(b, 1)
|
||||
defer c.Close()
|
||||
hldr := test.Holder{Holder: c[0].Server.Holder()}
|
||||
|
||||
indexName := "i"
|
||||
fieldName := "f"
|
||||
|
||||
index := hldr.MustCreateIndexIfNotExists(indexName, pilosa.IndexOptions{TrackExistence: nn})
|
||||
// Create field.
|
||||
if _, err := index.CreateFieldIfNotExists(fieldName); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
bitCount := 10000
|
||||
req := &pilosa.ImportRequest{
|
||||
Index: indexName,
|
||||
Field: fieldName,
|
||||
Shard: 0,
|
||||
RowIDs: make([]uint64, bitCount),
|
||||
ColumnIDs: make([]uint64, bitCount),
|
||||
}
|
||||
for i := 0; i < bitCount; i++ {
|
||||
req.RowIDs[i] = uint64(rand.Intn(100000))
|
||||
req.ColumnIDs[i] = uint64(rand.Intn(1 << 20))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := c[0].API.Import(context.Background(), req); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecutor_Existence_True(b *testing.B) { benchmarkExistence(true, b) }
|
||||
func BenchmarkExecutor_Existence_False(b *testing.B) { benchmarkExistence(false, b) }
|
||||
|
|
|
|||
29
field.go
29
field.go
|
|
@ -159,12 +159,17 @@ func OptFieldTypeMutex(cacheType string, cacheSize uint32) FieldOption {
|
|||
func NewField(path, index, name string, opts FieldOption) (*Field, error) {
|
||||
err := validateName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(err, "validating name")
|
||||
}
|
||||
|
||||
return newField(path, index, name, opts)
|
||||
}
|
||||
|
||||
// newField returns a new instance of field (without name validation).
|
||||
func newField(path, index, name string, opts FieldOption) (*Field, error) {
|
||||
// Apply functional option.
|
||||
fo := FieldOptions{}
|
||||
err = opts(&fo)
|
||||
err := opts(&fo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "applying option")
|
||||
}
|
||||
|
|
@ -1055,6 +1060,26 @@ func (f *Field) importValue(columnIDs []uint64, values []int64) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (f *Field) importRoaring(data []byte, shard uint64) error {
|
||||
viewName := viewStandard
|
||||
|
||||
view, err := f.createViewIfNotExists(viewName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating view")
|
||||
}
|
||||
|
||||
frag, err := view.CreateFragmentIfNotExists(shard)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating fragment")
|
||||
}
|
||||
|
||||
if err := frag.importRoaring(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type fieldSlice []*Field
|
||||
|
||||
func (p fieldSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
|
|
|||
189
fragment.go
189
fragment.go
|
|
@ -44,15 +44,19 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
// ShardWidth is the number of column IDs in a shard.
|
||||
ShardWidth = 1048576
|
||||
// ShardWidth is the number of column IDs in a shard. It must be a power of 2 greater than or equal to 16.
|
||||
shardWidthExponent = 20
|
||||
ShardWidth = 1 << shardWidthExponent
|
||||
|
||||
// containersPerRowSegment is dependent upon ShardWidth,
|
||||
// and it represents the number of containers per shard row
|
||||
// (or rowSegment). Since containers are set in roaring
|
||||
// to be 2^16, then this const should be ShardWidth / 2^16.
|
||||
// It is represented as the exponent n of 2^n.
|
||||
containersPerRowSegment = 4
|
||||
// shardVsContainerExponent is the power of 2 of ShardWith minus the power
|
||||
// of two of roaring container width (which is 16).
|
||||
// 2^shardVsContainerExponent is the number of containers in a shard row.
|
||||
//
|
||||
// It is represented in this rather awkward way because calculating the row
|
||||
// which a given container is in means dividing by the number of rows per
|
||||
// container which is performantly expressed as a right shift by this
|
||||
// exponent.
|
||||
shardVsContainerExponent = shardWidthExponent - 16
|
||||
|
||||
// snapshotExt is the file extension used for an in-process snapshot.
|
||||
snapshotExt = ".snapshotting"
|
||||
|
|
@ -334,15 +338,13 @@ func (f *fragment) closeStorage() error {
|
|||
func (f *fragment) row(rowID uint64) *Row {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.unprotectedRow(rowID, true, true)
|
||||
return f.unprotectedRow(rowID)
|
||||
}
|
||||
|
||||
func (f *fragment) unprotectedRow(rowID uint64, checkRowCache bool, updateRowCache bool) *Row {
|
||||
if checkRowCache {
|
||||
r, ok := f.rowCache.Fetch(rowID)
|
||||
if ok && r != nil {
|
||||
return r
|
||||
}
|
||||
func (f *fragment) unprotectedRow(rowID uint64) *Row {
|
||||
r, ok := f.rowCache.Fetch(rowID)
|
||||
if ok && r != nil {
|
||||
return r
|
||||
}
|
||||
|
||||
// Only use a subset of the containers.
|
||||
|
|
@ -361,9 +363,7 @@ func (f *fragment) unprotectedRow(rowID uint64, checkRowCache bool, updateRowCac
|
|||
}
|
||||
row.invalidateCount()
|
||||
|
||||
if updateRowCache {
|
||||
f.rowCache.Add(rowID, row)
|
||||
}
|
||||
f.rowCache.Add(rowID, row)
|
||||
|
||||
return row
|
||||
}
|
||||
|
|
@ -423,7 +423,7 @@ func (f *fragment) unprotectedSetBit(rowID, columnID uint64) (changed bool, err
|
|||
}
|
||||
|
||||
// Get the row from row cache or fragment.storage.
|
||||
row := f.unprotectedRow(rowID, true, true)
|
||||
row := f.unprotectedRow(rowID)
|
||||
row.SetBit(columnID)
|
||||
|
||||
// Update the cache.
|
||||
|
|
@ -475,7 +475,7 @@ func (f *fragment) unprotectedClearBit(rowID, columnID uint64) (changed bool, er
|
|||
}
|
||||
|
||||
// Get the row from cache or fragment.storage.
|
||||
row := f.unprotectedRow(rowID, true, true)
|
||||
row := f.unprotectedRow(rowID)
|
||||
row.clearBit(columnID)
|
||||
|
||||
// Update the cache.
|
||||
|
|
@ -1323,73 +1323,70 @@ func (f *fragment) mergeBlock(id int, data []pairSet) (sets, clears []pairSet, e
|
|||
}
|
||||
|
||||
// bulkImport bulk imports a set of bits and then snapshots the storage.
|
||||
// This does not affect the fragment's cache.
|
||||
// The cache is updated to reflect the new data.
|
||||
func (f *fragment) bulkImport(rowIDs, columnIDs []uint64) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
// Verify that there are an equal number of row ids and column ids.
|
||||
if len(rowIDs) != len(columnIDs) {
|
||||
return fmt.Errorf("mismatch of row/column len: %d != %d", len(rowIDs), len(columnIDs))
|
||||
}
|
||||
|
||||
// Create a temporary bitmap which will be populated by rowIDs and columnIDs
|
||||
// and then merged into the existing fragment's bitmap.
|
||||
localBitmap := roaring.NewBitmap()
|
||||
// Disconnect op writer so we don't append updates.
|
||||
f.storage.OpWriter = nil
|
||||
localBitmap.OpWriter = nil
|
||||
|
||||
// Process every bit.
|
||||
// If an error occurs then reopen the storage.
|
||||
lastID := uint64(0)
|
||||
if err := func() error {
|
||||
set := make(map[uint64]struct{})
|
||||
for i := range rowIDs {
|
||||
rowID, columnID := rowIDs[i], columnIDs[i]
|
||||
rowSet := make(map[uint64]struct{})
|
||||
for i := range rowIDs {
|
||||
rowID, columnID := rowIDs[i], columnIDs[i]
|
||||
|
||||
// Determine the position of the bit in the storage.
|
||||
pos, err := f.pos(rowID, columnID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting bit pos")
|
||||
}
|
||||
|
||||
// Write to storage.
|
||||
_, err = f.storage.Add(pos)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "writing")
|
||||
}
|
||||
// Reduce the StatsD rate for high volume stats
|
||||
f.stats.Count("ImportBit", 1, 0.0001)
|
||||
// import optimization to avoid linear foreach calls
|
||||
// slight risk of concurrent cache counter being off but
|
||||
// no real danger
|
||||
if i == 0 || rowID != lastID {
|
||||
lastID = rowID
|
||||
set[rowID] = struct{}{}
|
||||
}
|
||||
|
||||
// Invalidate block checksum.
|
||||
delete(f.checksums, int(rowID/HashBlockSize))
|
||||
// Determine the position of the bit in the storage.
|
||||
pos, err := f.pos(rowID, columnID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update cache counts for all rows.
|
||||
for rowID := range set {
|
||||
// Import should ALWAYS have row() load a new row from fragment.storage
|
||||
// because the row that's in rowCache hasn't been updated with
|
||||
// this import's data.
|
||||
f.cache.BulkAdd(rowID, f.unprotectedRow(rowID, false, false).Count())
|
||||
// Write to storage.
|
||||
_, err = localBitmap.Add(pos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Reduce the StatsD rate for high volume stats
|
||||
f.stats.Count("ImportBit", 1, 0.0001)
|
||||
// import optimization to avoid linear foreach calls
|
||||
// slight risk of concurrent cache counter being off but
|
||||
// no real danger
|
||||
if i == 0 || rowID != lastID {
|
||||
lastID = rowID
|
||||
rowSet[rowID] = struct{}{}
|
||||
}
|
||||
|
||||
f.cache.Invalidate()
|
||||
return nil
|
||||
}(); err != nil {
|
||||
_ = f.closeStorage()
|
||||
_ = f.openStorage()
|
||||
return err
|
||||
// Invalidate block checksum.
|
||||
delete(f.checksums, int(rowID/HashBlockSize))
|
||||
}
|
||||
|
||||
// Write the storage to disk and reload.
|
||||
if err := f.snapshot(); err != nil {
|
||||
return errors.Wrap(err, "snapshotting")
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
// Merge localBitmap into fragment's existing data.
|
||||
var results *roaring.Bitmap
|
||||
if f.storage.Count() > 0 {
|
||||
results = f.storage.Union(localBitmap)
|
||||
} else {
|
||||
results = localBitmap
|
||||
}
|
||||
|
||||
return nil
|
||||
// Update cache counts for all affected rows.
|
||||
for rowID := range rowSet {
|
||||
n := results.CountRange(rowID*ShardWidth, (rowID+1)*ShardWidth)
|
||||
f.cache.BulkAdd(rowID, n)
|
||||
}
|
||||
|
||||
f.cache.Recalculate()
|
||||
return unprotectedWriteToFragment(f, results)
|
||||
}
|
||||
|
||||
// importValue bulk imports a set of range-encoded values.
|
||||
|
|
@ -1425,6 +1422,51 @@ func (f *fragment) importValue(columnIDs, values []uint64, bitDepth uint) error
|
|||
return nil
|
||||
}
|
||||
|
||||
// importRoaring imports from the official roaring data format defined at
|
||||
// https://github.com/RoaringBitmap/RoaringFormatSpec or from pilosa's version
|
||||
// of the roaring format. The cache is updated to reflect the new data.
|
||||
func (f *fragment) importRoaring(data []byte) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
bm := roaring.NewBitmap()
|
||||
err := bm.UnmarshalBinary(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get a list of keys in order to update the cache
|
||||
iter, _ := bm.Containers.Iterator(0)
|
||||
rowSet := make([]uint64, 0)
|
||||
var lastRow uint64 = math.MaxUint64
|
||||
|
||||
for iter.Next() {
|
||||
key, _ := iter.Value()
|
||||
|
||||
// virtual row for the current container
|
||||
vRow := key >> shardVsContainerExponent
|
||||
|
||||
// skip dups
|
||||
if vRow == lastRow {
|
||||
continue
|
||||
}
|
||||
rowSet = append(rowSet, vRow)
|
||||
lastRow = vRow
|
||||
}
|
||||
|
||||
if f.storage.Count() > 0 {
|
||||
bm = f.storage.Union(bm)
|
||||
}
|
||||
|
||||
for _, rowID := range rowSet {
|
||||
n := bm.CountRange(rowID*ShardWidth, (rowID+1)*ShardWidth)
|
||||
f.cache.BulkAdd(rowID, n)
|
||||
}
|
||||
f.cache.Recalculate()
|
||||
|
||||
err = unprotectedWriteToFragment(f, bm)
|
||||
return err
|
||||
}
|
||||
|
||||
// incrementOpN increase the operation count by one.
|
||||
// If the count exceeds the maximum allowed then a snapshot is performed.
|
||||
func (f *fragment) incrementOpN() error {
|
||||
|
|
@ -1452,6 +1494,13 @@ func track(start time.Time, message string, stats StatsClient, logger Logger) {
|
|||
}
|
||||
|
||||
func (f *fragment) snapshot() error {
|
||||
return unprotectedWriteToFragment(f, f.storage)
|
||||
}
|
||||
|
||||
// unprotectedWriteToFragment writes the fragment f with bm as the data. It is unprotected, and
|
||||
// f.mu must be locked when calling it.
|
||||
func unprotectedWriteToFragment(f *fragment, bm *roaring.Bitmap) error { // nolint: interfacer
|
||||
|
||||
f.Logger.Printf("fragment: snapshotting %s/%s/%s/%d", f.index, f.field, f.view, f.shard)
|
||||
completeMessage := fmt.Sprintf("fragment: snapshot complete %s/%s/%s/%d", f.index, f.field, f.view, f.shard)
|
||||
start := time.Now()
|
||||
|
|
@ -1467,7 +1516,7 @@ func (f *fragment) snapshot() error {
|
|||
|
||||
// Write storage to snapshot.
|
||||
bw := bufio.NewWriter(file)
|
||||
if _, err := f.storage.WriteTo(bw); err != nil {
|
||||
if _, err := bm.WriteTo(bw); err != nil {
|
||||
return fmt.Errorf("snapshot write to: %s", err)
|
||||
}
|
||||
|
||||
|
|
@ -1720,7 +1769,7 @@ func (f *fragment) rows() []uint64 {
|
|||
key, _ := i.Value()
|
||||
|
||||
// virtual row for the current container
|
||||
vRow := key >> containersPerRowSegment
|
||||
vRow := key >> shardVsContainerExponent
|
||||
|
||||
// skip dups
|
||||
if vRow == lastRow {
|
||||
|
|
@ -1749,7 +1798,7 @@ func (f *fragment) rowsForColumn(columnID uint64) []uint64 {
|
|||
key, c := i.Value()
|
||||
|
||||
// virtual row for the current container
|
||||
vRow := key >> containersPerRowSegment
|
||||
vRow := key >> shardVsContainerExponent
|
||||
|
||||
// column container key for virtual row
|
||||
colKey = ((vRow * ShardWidth) + colID) >> 16
|
||||
|
|
|
|||
|
|
@ -17,14 +17,17 @@ package pilosa
|
|||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/pilosa/pilosa/pql"
|
||||
"github.com/pilosa/pilosa/roaring"
|
||||
)
|
||||
|
||||
// Test flags
|
||||
|
|
@ -746,7 +749,7 @@ func TestFragment_TopN_CacheSize(t *testing.T) {
|
|||
cacheSize := uint32(3)
|
||||
|
||||
// Create Index.
|
||||
index := mustOpenIndex()
|
||||
index := mustOpenIndex(IndexOptions{})
|
||||
defer index.Close()
|
||||
|
||||
// Create field.
|
||||
|
|
@ -912,7 +915,7 @@ func TestFragment_LRUCache_Persistence(t *testing.T) {
|
|||
|
||||
// Ensure a fragment's cache can be persisted between restarts.
|
||||
func TestFragment_RankCache_Persistence(t *testing.T) {
|
||||
index := mustOpenIndex()
|
||||
index := mustOpenIndex(IndexOptions{})
|
||||
defer index.Close()
|
||||
|
||||
// Create field.
|
||||
|
|
@ -1397,3 +1400,200 @@ func TestFragment_RowsIteration(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test Importing roaring data.
|
||||
func TestFragment_RoaringImport(t *testing.T) {
|
||||
tests := [][][]uint64{
|
||||
{
|
||||
[]uint64{0},
|
||||
[]uint64{1},
|
||||
},
|
||||
{
|
||||
[]uint64{0, 65535, 65536, 65537, 65538, 65539, 130000},
|
||||
[]uint64{1000, 67000, 130000},
|
||||
},
|
||||
{
|
||||
[]uint64{0, 65535, 65536, 65537, 65538, 65539, 130000},
|
||||
[]uint64{0, 65535, 65536, 65537, 65538, 65539, 130000},
|
||||
},
|
||||
{
|
||||
[]uint64{0, 65535, 65536, 1<<20 + 1, 1<<20*2 + 1},
|
||||
[]uint64{1, 1<<20 + 2, 1<<20*2 + 2},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("importroaring%d", i), func(t *testing.T) {
|
||||
f := mustOpenFragment("i", "f", viewStandard, 0, "")
|
||||
defer f.Close()
|
||||
for num, input := range test {
|
||||
buf := &bytes.Buffer{}
|
||||
bm := roaring.NewBitmap(input...)
|
||||
_, err := bm.WriteTo(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("writing to buffer: %v", err)
|
||||
}
|
||||
f.importRoaring(buf.Bytes())
|
||||
exp := calcExpected(test[:num+1]...)
|
||||
for row, expCols := range exp {
|
||||
cols := f.row(uint64(row)).Columns()
|
||||
t.Logf("\nrow: %d\n exp:%v\n got:%v", row, expCols, cols)
|
||||
if !reflect.DeepEqual(cols, expCols) {
|
||||
t.Fatalf("input%d, row %d\n exp:%v\n got:%v", num, row, expCols, cols)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test Importing roaring data.
|
||||
func TestFragment_RoaringImportTopN(t *testing.T) {
|
||||
tests := []struct {
|
||||
rowIDs []uint64
|
||||
colIDs []uint64
|
||||
rowIDs2 []uint64
|
||||
colIDs2 []uint64
|
||||
roaring []uint64
|
||||
}{
|
||||
{
|
||||
rowIDs: []uint64{4, 4, 4, 4},
|
||||
colIDs: []uint64{0, 1, 2, 3},
|
||||
rowIDs2: []uint64{5, 5, 5, 5, 5},
|
||||
colIDs2: []uint64{0, 1, 2, 3, 4},
|
||||
roaring: []uint64{0, 65535, 65536, 1<<20 + 1, 1<<20 + 2, 1<<20*2 + 1},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("importroaring%d", i), func(t *testing.T) {
|
||||
f := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked)
|
||||
defer f.Close()
|
||||
|
||||
err := f.bulkImport(test.rowIDs, test.colIDs)
|
||||
if err != nil {
|
||||
t.Fatalf("bulk importing ids: %v", err)
|
||||
}
|
||||
expPairs := calcTop(test.rowIDs, test.colIDs)
|
||||
pairs, err := f.top(topOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("executing top after bulk import: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(expPairs, pairs) {
|
||||
t.Fatalf("post bulk import:\n exp: %v\n got: %v\n", expPairs, pairs)
|
||||
}
|
||||
|
||||
err = f.bulkImport(test.rowIDs2, test.colIDs2)
|
||||
if err != nil {
|
||||
t.Fatalf("bulk importing ids: %v", err)
|
||||
}
|
||||
test.rowIDs = append(test.rowIDs, test.rowIDs2...)
|
||||
test.colIDs = append(test.colIDs, test.colIDs2...)
|
||||
expPairs = calcTop(test.rowIDs, test.colIDs)
|
||||
pairs, err = f.top(topOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("executing top after bulk import: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(expPairs, pairs) {
|
||||
t.Fatalf("post bulk import2:\n exp: %v\n got: %v\n", expPairs, pairs)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
bm := roaring.NewBitmap(test.roaring...)
|
||||
_, err = bm.WriteTo(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("writing to buffer: %v", err)
|
||||
}
|
||||
f.importRoaring(buf.Bytes())
|
||||
rows, cols := toRowsCols(test.roaring)
|
||||
expPairs = calcTop(append(test.rowIDs, rows...), append(test.colIDs, cols...))
|
||||
pairs, err = f.top(topOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("executing top after roaring import: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(expPairs, pairs) {
|
||||
t.Fatalf("post Roaring:\n exp: %v\n got: %v\n", expPairs, pairs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func toRowsCols(roaring []uint64) (rowIDs, colIDs []uint64) {
|
||||
rowIDs, colIDs = make([]uint64, len(roaring)), make([]uint64, len(roaring))
|
||||
for i, bit := range roaring {
|
||||
rowIDs[i] = bit / ShardWidth
|
||||
colIDs[i] = bit % ShardWidth
|
||||
}
|
||||
return rowIDs, colIDs
|
||||
}
|
||||
|
||||
func calcTop(rowIDs, colIDs []uint64) []Pair {
|
||||
if len(rowIDs) != len(colIDs) {
|
||||
panic("row and col ids must be of equal len")
|
||||
}
|
||||
// make map of rowID to colID set in order to dedup
|
||||
counts := make(map[uint64]map[uint64]struct{})
|
||||
for i := 0; i < len(rowIDs); i++ {
|
||||
row, col := rowIDs[i], colIDs[i]
|
||||
m, ok := counts[row]
|
||||
if !ok {
|
||||
m = make(map[uint64]struct{})
|
||||
counts[row] = m
|
||||
}
|
||||
m[col] = struct{}{}
|
||||
}
|
||||
|
||||
// build slice of pairs from map
|
||||
ret := make([]Pair, 0)
|
||||
for row, cols := range counts {
|
||||
ret = append(ret, Pair{ID: row, Count: uint64(len(cols))})
|
||||
}
|
||||
|
||||
// reverse sort by count
|
||||
sort.Slice(ret, func(i, j int) bool { return ret[i].Count > ret[j].Count })
|
||||
return ret
|
||||
}
|
||||
|
||||
// calcExpected takes a number of slices of uint64 represented data to be added
|
||||
// to a fragment. It calculates which rows and bits set in each row would be
|
||||
// expected after importing that data, and returns a [][]uint64 where the index
|
||||
// into the first slice is row id.
|
||||
func calcExpected(inputs ...[]uint64) [][]uint64 {
|
||||
// create map of row id to set of column values in that row.
|
||||
rows := make(map[uint64]map[uint64]struct{})
|
||||
var maxrow uint64 = 0
|
||||
for _, input := range inputs {
|
||||
for _, val := range input {
|
||||
row := val / ShardWidth
|
||||
if row > maxrow {
|
||||
maxrow = row
|
||||
}
|
||||
m, ok := rows[row]
|
||||
if !ok {
|
||||
m = make(map[uint64]struct{})
|
||||
rows[row] = m
|
||||
}
|
||||
m[val%ShardWidth] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// initialize ret slice
|
||||
ret := make([][]uint64, maxrow+1)
|
||||
for i := range ret {
|
||||
ret[i] = make([]uint64, 0)
|
||||
}
|
||||
|
||||
// populate ret slices from rows map
|
||||
for row, vals := range rows {
|
||||
for val := range vals {
|
||||
ret[row] = append(ret[row], val)
|
||||
}
|
||||
}
|
||||
|
||||
// sort ret slices
|
||||
for _, slice := range ret {
|
||||
sort.Slice(slice, func(i int, j int) bool { return slice[i] < slice[j] })
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ const (
|
|||
|
||||
// fileLimit is the maximum open file limit (ulimit -n) to automatically set.
|
||||
fileLimit = 262144 // (512^2)
|
||||
|
||||
// existenceFieldName is the name of the internal field used to store existence values.
|
||||
existenceFieldName = "exists"
|
||||
)
|
||||
|
||||
// Holder represents a container for indexes.
|
||||
|
|
@ -354,6 +357,7 @@ func (h *Holder) createIndex(name string, opt IndexOptions) (*Index, error) {
|
|||
}
|
||||
|
||||
index.keys = opt.Keys
|
||||
index.trackExistence = opt.TrackExistence
|
||||
|
||||
if err := index.Open(); err != nil {
|
||||
return nil, errors.Wrap(err, "opening")
|
||||
|
|
|
|||
|
|
@ -372,7 +372,11 @@ func (c *InternalClient) EnsureIndex(ctx context.Context, name string, options p
|
|||
}
|
||||
|
||||
func (c *InternalClient) EnsureField(ctx context.Context, indexName string, fieldName string) error {
|
||||
err := c.CreateField(ctx, indexName, fieldName)
|
||||
return c.EnsureFieldWithOptions(ctx, indexName, fieldName, pilosa.FieldOptions{})
|
||||
}
|
||||
|
||||
func (c *InternalClient) EnsureFieldWithOptions(ctx context.Context, indexName string, fieldName string, opt pilosa.FieldOptions) error {
|
||||
err := c.CreateFieldWithOptions(ctx, indexName, fieldName, opt)
|
||||
if err == nil || errors.Cause(err) == pilosa.ErrFieldExists {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -475,12 +479,6 @@ func (c *InternalClient) ImportValue(ctx context.Context, index, field string, s
|
|||
|
||||
// ImportValueK bulk imports keyed field values to a host.
|
||||
func (c *InternalClient) ImportValueK(ctx context.Context, index, field string, vals []pilosa.FieldValue) error {
|
||||
if index == "" {
|
||||
return pilosa.ErrIndexRequired
|
||||
} else if field == "" {
|
||||
return pilosa.ErrFieldRequired
|
||||
}
|
||||
|
||||
buf, err := c.marshalImportValuePayload(index, field, 0, vals)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error Creating Payload: %s", err)
|
||||
|
|
@ -527,6 +525,49 @@ func (c *InternalClient) marshalImportValuePayload(index, field string, shard ui
|
|||
return buf, nil
|
||||
}
|
||||
|
||||
// ImportRoaring does fast import of raw bits in roaring format (pilosa or
|
||||
// official format, see API.ImportRoaring).
|
||||
func (c *InternalClient) ImportRoaring(ctx context.Context, uri *pilosa.URI, index, field string, shard uint64, remote bool, data []byte) error {
|
||||
if index == "" {
|
||||
return pilosa.ErrIndexRequired
|
||||
} else if field == "" {
|
||||
return pilosa.ErrFieldRequired
|
||||
}
|
||||
if uri == nil {
|
||||
uri = c.defaultURI
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/index/%s/field/%s/import-roaring/%d?remote=%v", uri, index, field, shard, remote)
|
||||
|
||||
// Generate HTTP request.
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating request")
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-binary")
|
||||
req.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
|
||||
|
||||
// Execute request against the host.
|
||||
resp, err := c.httpClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "executing request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Validate status code.
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("invalid status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
rbody := &pilosa.ImportResponse{}
|
||||
dec.Decode(rbody)
|
||||
if rbody.Err != "" {
|
||||
return errors.Errorf("importing roaring: %v", rbody.Err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExportCSV bulk exports data for a single shard from a host to CSV format.
|
||||
func (c *InternalClient) ExportCSV(ctx context.Context, index, field string, shard uint64, w io.Writer) error {
|
||||
if index == "" {
|
||||
|
|
@ -636,16 +677,35 @@ func (c *InternalClient) backupShardNode(ctx context.Context, index, field strin
|
|||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// CreateField creates a new field on the server.
|
||||
func (c *InternalClient) CreateField(ctx context.Context, index, field string) error {
|
||||
return c.CreateFieldWithOptions(ctx, index, field, pilosa.FieldOptions{})
|
||||
}
|
||||
|
||||
// CreateField creates a new field on the server.
|
||||
func (c *InternalClient) CreateFieldWithOptions(ctx context.Context, index, field string, opt pilosa.FieldOptions) error {
|
||||
if index == "" {
|
||||
return pilosa.ErrIndexRequired
|
||||
}
|
||||
|
||||
// convert pilosa.FieldOptions to fieldOptions
|
||||
fieldOpt := fieldOptions{
|
||||
Type: opt.Type,
|
||||
Keys: &opt.Keys,
|
||||
}
|
||||
if fieldOpt.Type == "set" {
|
||||
fieldOpt.CacheType = &opt.CacheType
|
||||
fieldOpt.CacheSize = &opt.CacheSize
|
||||
} else if fieldOpt.Type == "int" {
|
||||
fieldOpt.Min = &opt.Min
|
||||
fieldOpt.Max = &opt.Max
|
||||
} else if fieldOpt.Type == "time" {
|
||||
fieldOpt.TimeQuantum = &opt.TimeQuantum
|
||||
}
|
||||
|
||||
// TODO: remove buf completely? (depends on whether importer needs to create specific field types)
|
||||
// Encode query request.
|
||||
buf, err := json.Marshal(&postFieldRequest{
|
||||
//Options: opt,
|
||||
Options: fieldOpt,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshaling")
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
gohttp "net/http"
|
||||
"reflect"
|
||||
|
|
@ -196,7 +197,7 @@ func TestClient_Export(t *testing.T) {
|
|||
{RowID: 2, ColumnID: 203, RowKey: "row2", ColumnKey: "col203"},
|
||||
}
|
||||
|
||||
t.Run("Import unkeyed,unkeyedf", func(t *testing.T) {
|
||||
t.Run("Export unkeyed,unkeyedf", func(t *testing.T) {
|
||||
// Populate data.
|
||||
for _, bit := range data {
|
||||
_, err := c.Query(context.Background(), "unkeyed", &pilosa.QueryRequest{
|
||||
|
|
@ -230,7 +231,7 @@ func TestClient_Export(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("Import unkeyed,keyedf", func(t *testing.T) {
|
||||
t.Run("Export unkeyed,keyedf", func(t *testing.T) {
|
||||
// Populate data.
|
||||
for _, bit := range data {
|
||||
_, err := c.Query(context.Background(), "unkeyed", &pilosa.QueryRequest{
|
||||
|
|
@ -264,7 +265,7 @@ func TestClient_Export(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("Import keyed,unkeyedf", func(t *testing.T) {
|
||||
t.Run("Export keyed,unkeyedf", func(t *testing.T) {
|
||||
// Populate data.
|
||||
for _, bit := range data {
|
||||
_, err := c.Query(context.Background(), "keyed", &pilosa.QueryRequest{
|
||||
|
|
@ -298,7 +299,7 @@ func TestClient_Export(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("Import keyed,keyedf", func(t *testing.T) {
|
||||
t.Run("Export keyed,keyedf", func(t *testing.T) {
|
||||
// Populate data.
|
||||
for _, bit := range data {
|
||||
_, err := c.Query(context.Background(), "keyed", &pilosa.QueryRequest{
|
||||
|
|
@ -363,6 +364,58 @@ func TestClient_Import(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure client can bulk import data.
|
||||
func TestClient_ImportRoaring(t *testing.T) {
|
||||
cluster := test.MustNewCluster(t, 2)
|
||||
for _, c := range cluster {
|
||||
c.Config.Cluster.ReplicaN = 2
|
||||
}
|
||||
err := cluster.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("starting cluster: %v", err)
|
||||
}
|
||||
defer cluster.Close()
|
||||
|
||||
_, err = cluster[0].API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("creating index: %v", err)
|
||||
}
|
||||
_, err = cluster[0].API.CreateField(context.Background(), "i", "f", pilosa.OptFieldTypeSet(pilosa.CacheTypeRanked, 100))
|
||||
if err != nil {
|
||||
t.Fatalf("creating field: %v", err)
|
||||
}
|
||||
_, err = cluster[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: "Set(0, f=1)"})
|
||||
if err != nil {
|
||||
t.Fatalf("querying: %v", err)
|
||||
}
|
||||
|
||||
// Send import request.
|
||||
host := cluster[0].URL()
|
||||
c := MustNewClient(host, http.GetHTTPClient(nil))
|
||||
roaringData, _ := hex.DecodeString("3B3001000100000900010000000100010009000100")
|
||||
if err := c.ImportRoaring(context.Background(), &cluster[0].API.Node().URI, "i", "f", 0, false, roaringData); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hldr := test.Holder{Holder: cluster[0].Server.Holder()}
|
||||
// Verify data.
|
||||
if a := hldr.Row("i", "f", 0).Columns(); !reflect.DeepEqual(a, []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 65537}) {
|
||||
t.Fatalf("unexpected columns: %+v", a)
|
||||
}
|
||||
if a := hldr.Row("i", "f", 1).Columns(); !reflect.DeepEqual(a, []uint64{0}) {
|
||||
t.Fatalf("unexpected columns: %+v", a)
|
||||
}
|
||||
|
||||
hldr2 := test.Holder{Holder: cluster[1].Server.Holder()}
|
||||
// Verify data.
|
||||
if a := hldr2.Row("i", "f", 0).Columns(); !reflect.DeepEqual(a, []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 65537}) {
|
||||
t.Fatalf("unexpected columns: %+v", a)
|
||||
}
|
||||
if a := hldr2.Row("i", "f", 1).Columns(); !reflect.DeepEqual(a, []uint64{0}) {
|
||||
t.Fatalf("unexpected columns: %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure client can bulk import data.
|
||||
func TestClient_ImportKeys(t *testing.T) {
|
||||
t.Run("SingleNode", func(t *testing.T) {
|
||||
|
|
@ -655,6 +708,83 @@ func TestClient_ImportValue(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure client can bulk import data while tracking existence.
|
||||
func TestClient_ImportExistence(t *testing.T) {
|
||||
cmd := test.MustRunCluster(t, 1)[0]
|
||||
host := cmd.URL()
|
||||
holder := cmd.Server.Holder()
|
||||
hldr := test.Holder{Holder: holder}
|
||||
|
||||
t.Run("Set", func(t *testing.T) {
|
||||
idxName := "iset"
|
||||
fldName := "fset"
|
||||
|
||||
index := hldr.MustCreateIndexIfNotExists(idxName, pilosa.IndexOptions{TrackExistence: true})
|
||||
_, err := index.CreateFieldIfNotExists(fldName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Send import request.
|
||||
c := MustNewClient(host, http.GetHTTPClient(nil))
|
||||
if err := c.Import(context.Background(), idxName, fldName, 0, []pilosa.Bit{
|
||||
{RowID: 0, ColumnID: 1},
|
||||
{RowID: 0, ColumnID: 5},
|
||||
{RowID: 200, ColumnID: 6},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify data.
|
||||
if a := hldr.Row(idxName, fldName, 0).Columns(); !reflect.DeepEqual(a, []uint64{1, 5}) {
|
||||
t.Fatalf("unexpected columns: %+v", a)
|
||||
}
|
||||
if a := hldr.Row(idxName, fldName, 200).Columns(); !reflect.DeepEqual(a, []uint64{6}) {
|
||||
t.Fatalf("unexpected columns: %+v", a)
|
||||
}
|
||||
|
||||
// Verify existence.
|
||||
if a := hldr.ReadRow(idxName, "exists", 0).Columns(); !reflect.DeepEqual(a, []uint64{1, 5, 6}) {
|
||||
t.Fatalf("unexpected existence columns: %+v", a)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Int", func(t *testing.T) {
|
||||
idxName := "iint"
|
||||
fldName := "fint"
|
||||
|
||||
index := hldr.MustCreateIndexIfNotExists(idxName, pilosa.IndexOptions{TrackExistence: true})
|
||||
field, err := index.CreateFieldIfNotExists(fldName, pilosa.OptFieldTypeInt(-100, 100))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Send import request.
|
||||
c := MustNewClient(host, http.GetHTTPClient(nil))
|
||||
if err := c.ImportValue(context.Background(), idxName, fldName, 0, []pilosa.FieldValue{
|
||||
{ColumnID: 1, Value: -10},
|
||||
{ColumnID: 2, Value: 20},
|
||||
{ColumnID: 3, Value: 40},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify Sum.
|
||||
sum, cnt, err := field.Sum(nil, fldName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sum != 50 || cnt != 3 {
|
||||
t.Fatalf("unexpected values: got sum=%v, count=%v; expected sum=50, cnt=3", sum, cnt)
|
||||
}
|
||||
|
||||
// Verify existence.
|
||||
if a := hldr.ReadRow(idxName, "exists", 0).Columns(); !reflect.DeepEqual(a, []uint64{1, 2, 3}) {
|
||||
t.Fatalf("unexpected existence columns: %+v", a)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure client can retrieve a list of all checksums for blocks in a fragment.
|
||||
func TestClient_FragmentBlocks(t *testing.T) {
|
||||
cmd := test.MustRunCluster(t, 1)[0]
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ func (h *Handler) populateValidators() {
|
|||
func (h *Handler) queryArgValidator(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
key := mux.CurrentRoute(r).GetName()
|
||||
|
||||
if validator, ok := h.validators[key]; ok {
|
||||
if err := validator.validate(r.URL.Query()); err != nil {
|
||||
// TODO: Return the response depending on the Accept header
|
||||
|
|
@ -217,6 +218,7 @@ func newRouter(handler *Handler) *mux.Router {
|
|||
router.HandleFunc("/index/{index}/field/{field}", handler.handlePostField).Methods("POST")
|
||||
router.HandleFunc("/index/{index}/field/{field}", handler.handleDeleteField).Methods("DELETE")
|
||||
router.HandleFunc("/index/{index}/field/{field}/import", handler.handlePostImport).Methods("POST")
|
||||
router.HandleFunc("/index/{index}/field/{field}/import-roaring/{shard}", handler.handlePostImportRoaring).Methods("POST")
|
||||
router.HandleFunc("/index/{index}/query", handler.handlePostQuery).Methods("POST").Name("PostQuery")
|
||||
router.HandleFunc("/info", handler.handleGetInfo).Methods("GET")
|
||||
router.HandleFunc("/recalculate-caches", handler.handleRecalculateCaches).Methods("POST")
|
||||
|
|
@ -1427,3 +1429,50 @@ func GetHTTPClient(t *tls.Config) *http.Client {
|
|||
}
|
||||
return &http.Client{Transport: transport}
|
||||
}
|
||||
|
||||
// handlPostRoaringImport
|
||||
func (h *Handler) handlePostImportRoaring(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Content-Type") != "application/x-binary" {
|
||||
http.Error(w, "Unsupported media type", http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
remoteStr := q.Get("remote")
|
||||
var remote bool
|
||||
if remoteStr == "true" {
|
||||
remote = true
|
||||
}
|
||||
|
||||
// Read entire body.
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
urlVars := mux.Vars(r)
|
||||
shard, err := strconv.ParseUint(urlVars["shard"], 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "shard should be an unsigned integer", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO give meaningful stats for import
|
||||
err = h.api.ImportRoaring(r.Context(), urlVars["index"], urlVars["field"], shard, remote, body)
|
||||
resp := &pilosa.ImportResponse{}
|
||||
if err != nil {
|
||||
resp.Err = err.Error()
|
||||
}
|
||||
// Marshal response object.
|
||||
buf, err := h.api.Serializer.Marshal(resp)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("marshal import response: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Write response.
|
||||
_, err = w.Write(buf)
|
||||
if err != nil {
|
||||
h.logger.Printf("writing import-roaring response: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
index.go
64
index.go
|
|
@ -36,6 +36,10 @@ type Index struct {
|
|||
name string
|
||||
keys bool // use string keys
|
||||
|
||||
// Existence tracking.
|
||||
trackExistence bool
|
||||
existenceFld *Field
|
||||
|
||||
// Fields by name.
|
||||
fields map[string]*Field
|
||||
|
||||
|
|
@ -91,7 +95,10 @@ func (i *Index) Options() IndexOptions {
|
|||
}
|
||||
|
||||
func (i *Index) options() IndexOptions {
|
||||
return IndexOptions{Keys: i.keys}
|
||||
return IndexOptions{
|
||||
Keys: i.keys,
|
||||
TrackExistence: i.trackExistence,
|
||||
}
|
||||
}
|
||||
|
||||
// Open opens and initializes the index.
|
||||
|
|
@ -110,6 +117,12 @@ func (i *Index) Open() error {
|
|||
return errors.Wrap(err, "opening fields")
|
||||
}
|
||||
|
||||
if i.trackExistence {
|
||||
if err := i.openExistenceField(); err != nil {
|
||||
return errors.Wrap(err, "opening existence field")
|
||||
}
|
||||
}
|
||||
|
||||
if err := i.columnAttrs.Open(); err != nil {
|
||||
return errors.Wrap(err, "opening attrstore")
|
||||
}
|
||||
|
|
@ -147,6 +160,16 @@ func (i *Index) openFields() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// openExistenceField gets or creates the existence field and associates it to the index.
|
||||
func (i *Index) openExistenceField() error {
|
||||
f, err := i.createFieldIfNotExists(existenceFieldName, FieldOptions{CacheType: CacheTypeNone, CacheSize: 0})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating existence field")
|
||||
}
|
||||
i.existenceFld = f
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadMeta reads meta data for the index, if any.
|
||||
func (i *Index) loadMeta() error {
|
||||
var pb internal.IndexMeta
|
||||
|
|
@ -165,6 +188,7 @@ func (i *Index) loadMeta() error {
|
|||
|
||||
// Copy metadata fields.
|
||||
i.keys = pb.Keys
|
||||
i.trackExistence = pb.TrackExistence
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -173,7 +197,8 @@ func (i *Index) loadMeta() error {
|
|||
func (i *Index) saveMeta() error {
|
||||
// Marshal metadata.
|
||||
buf, err := proto.Marshal(&internal.IndexMeta{
|
||||
Keys: i.keys,
|
||||
Keys: i.keys,
|
||||
TrackExistence: i.trackExistence,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshalling")
|
||||
|
|
@ -250,6 +275,14 @@ func (i *Index) Fields() []*Field {
|
|||
return a
|
||||
}
|
||||
|
||||
// existenceField returns the internal field used to track column existence.
|
||||
func (i *Index) existenceField() *Field {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
return i.existenceFld
|
||||
}
|
||||
|
||||
// recalculateCaches recalculates caches on every field in the index.
|
||||
func (i *Index) recalculateCaches() {
|
||||
for _, field := range i.Fields() {
|
||||
|
|
@ -259,6 +292,11 @@ func (i *Index) recalculateCaches() {
|
|||
|
||||
// CreateField creates a field.
|
||||
func (i *Index) CreateField(name string, opts ...FieldOption) (*Field, error) {
|
||||
err := validateName(name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "validating name")
|
||||
}
|
||||
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
|
|
@ -281,6 +319,11 @@ func (i *Index) CreateField(name string, opts ...FieldOption) (*Field, error) {
|
|||
|
||||
// CreateFieldIfNotExists creates a field with the given options if it doesn't exist.
|
||||
func (i *Index) CreateFieldIfNotExists(name string, opts ...FieldOption) (*Field, error) {
|
||||
err := validateName(name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "validating name")
|
||||
}
|
||||
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
|
|
@ -349,7 +392,7 @@ func (i *Index) createField(name string, opt FieldOptions) (*Field, error) {
|
|||
}
|
||||
|
||||
func (i *Index) newField(path, name string) (*Field, error) {
|
||||
f, err := NewField(path, i.name, name, OptFieldTypeDefault())
|
||||
f, err := newField(path, i.name, name, OptFieldTypeDefault())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -381,6 +424,18 @@ func (i *Index) DeleteField(name string) error {
|
|||
return errors.Wrap(err, "removing directory")
|
||||
}
|
||||
|
||||
// If the field being deleted is the existence field,
|
||||
// turn off existence tracking on the index.
|
||||
if name == existenceFieldName {
|
||||
i.trackExistence = false
|
||||
i.existenceFld = nil
|
||||
|
||||
// Update meta data on disk.
|
||||
if err := i.saveMeta(); err != nil {
|
||||
return errors.Wrap(err, "saving existence meta data")
|
||||
}
|
||||
}
|
||||
|
||||
// Remove reference.
|
||||
delete(i.fields, name)
|
||||
|
||||
|
|
@ -408,7 +463,8 @@ func (p indexInfoSlice) Less(i, j int) bool { return p[i].Name < p[j].Name }
|
|||
|
||||
// IndexOptions represents options to set when initializing an index.
|
||||
type IndexOptions struct {
|
||||
Keys bool `json:"keys"`
|
||||
Keys bool `json:"keys"`
|
||||
TrackExistence bool `json:"trackExistence"`
|
||||
}
|
||||
|
||||
// hasTime returns true if a contains a non-nil time.
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@ package pilosa
|
|||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mustOpenIndex returns a new, opened index at a temporary path. Panic on error.
|
||||
func mustOpenIndex() *Index {
|
||||
func mustOpenIndex(opt IndexOptions) *Index {
|
||||
path, err := ioutil.TempDir("", "pilosa-index-")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
@ -28,6 +29,10 @@ func mustOpenIndex() *Index {
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
index.keys = opt.Keys
|
||||
index.trackExistence = opt.TrackExistence
|
||||
|
||||
if err := index.Open(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
@ -44,3 +49,40 @@ func (i *Index) reopen() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure that deleting the existence field is handled properly.
|
||||
func TestIndex_Existence_Delete(t *testing.T) {
|
||||
// Create Index (with existence tracking).
|
||||
index := mustOpenIndex(IndexOptions{TrackExistence: true})
|
||||
defer index.Close()
|
||||
|
||||
// Ensure existence field has been created.
|
||||
ef := index.Field(existenceFieldName)
|
||||
if ef == nil {
|
||||
t.Fatalf("expected field to have been created: %s", existenceFieldName)
|
||||
} else if !index.trackExistence {
|
||||
t.Fatalf("expected index.trackExistence to be true")
|
||||
} else if index.existenceFld == nil {
|
||||
t.Fatalf("expected index.existenceField to be non-nil")
|
||||
}
|
||||
|
||||
// Delete existence field.
|
||||
if err := index.DeleteField(existenceFieldName); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Re-open index.
|
||||
if err := index.reopen(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ensure existence field no longer exists.
|
||||
ef = index.Field(existenceFieldName)
|
||||
if ef != nil {
|
||||
t.Fatalf("expected field to have been deleted: %s", existenceFieldName)
|
||||
} else if index.trackExistence {
|
||||
t.Fatalf("expected index.trackExistence to be false")
|
||||
} else if index.existenceFld != nil {
|
||||
t.Fatalf("expected index.existenceField to be nil")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@ var _ = math.Inf
|
|||
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||
|
||||
type IndexMeta struct {
|
||||
Keys bool `protobuf:"varint,3,opt,name=Keys,proto3" json:"Keys,omitempty"`
|
||||
Keys bool `protobuf:"varint,3,opt,name=Keys,proto3" json:"Keys,omitempty"`
|
||||
TrackExistence bool `protobuf:"varint,4,opt,name=TrackExistence,proto3" json:"TrackExistence,omitempty"`
|
||||
}
|
||||
|
||||
func (m *IndexMeta) Reset() { *m = IndexMeta{} }
|
||||
|
|
@ -77,6 +78,13 @@ func (m *IndexMeta) GetKeys() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (m *IndexMeta) GetTrackExistence() bool {
|
||||
if m != nil {
|
||||
return m.TrackExistence
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type FieldOptions struct {
|
||||
Type string `protobuf:"bytes,8,opt,name=Type,proto3" json:"Type,omitempty"`
|
||||
CacheType string `protobuf:"bytes,3,opt,name=CacheType,proto3" json:"CacheType,omitempty"`
|
||||
|
|
@ -1051,6 +1059,16 @@ func (m *IndexMeta) MarshalTo(dAtA []byte) (int, error) {
|
|||
}
|
||||
i++
|
||||
}
|
||||
if m.TrackExistence {
|
||||
dAtA[i] = 0x20
|
||||
i++
|
||||
if m.TrackExistence {
|
||||
dAtA[i] = 1
|
||||
} else {
|
||||
dAtA[i] = 0
|
||||
}
|
||||
i++
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
|
|
@ -2300,6 +2318,9 @@ func (m *IndexMeta) Size() (n int) {
|
|||
if m.Keys {
|
||||
n += 2
|
||||
}
|
||||
if m.TrackExistence {
|
||||
n += 2
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
|
|
@ -2902,6 +2923,26 @@ func (m *IndexMeta) Unmarshal(dAtA []byte) error {
|
|||
}
|
||||
}
|
||||
m.Keys = bool(v != 0)
|
||||
case 4:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field TrackExistence", wireType)
|
||||
}
|
||||
var v int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowPrivate
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
v |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
m.TrackExistence = bool(v != 0)
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipPrivate(dAtA[iNdEx:])
|
||||
|
|
@ -7143,73 +7184,74 @@ var (
|
|||
func init() { proto.RegisterFile("private.proto", fileDescriptorPrivate) }
|
||||
|
||||
var fileDescriptorPrivate = []byte{
|
||||
// 1077 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x56, 0xdd, 0x6e, 0x1b, 0x45,
|
||||
0x14, 0x66, 0x7f, 0xec, 0xda, 0xc7, 0x75, 0x9a, 0x6c, 0x69, 0xd8, 0x22, 0x94, 0x9a, 0x51, 0xa5,
|
||||
0x9a, 0x4a, 0x84, 0xaa, 0xbd, 0xe1, 0xaf, 0x52, 0x49, 0x1c, 0x60, 0x29, 0x09, 0x65, 0x36, 0xc9,
|
||||
0x5d, 0x2f, 0x26, 0xf6, 0xa8, 0x59, 0x65, 0xbd, 0xb3, 0xec, 0xce, 0x26, 0x71, 0x2f, 0xb8, 0x05,
|
||||
0x89, 0x17, 0x40, 0x3c, 0x09, 0x8f, 0xc0, 0x25, 0x8f, 0x80, 0xc2, 0x8b, 0xa0, 0x39, 0x33, 0xfb,
|
||||
0x13, 0xc7, 0x21, 0x55, 0xe0, 0x6e, 0xce, 0x77, 0xce, 0x9c, 0xf3, 0xed, 0xf9, 0x9b, 0x85, 0x7e,
|
||||
0x9a, 0x45, 0xc7, 0x4c, 0xf2, 0xf5, 0x34, 0x13, 0x52, 0x78, 0x9d, 0x28, 0x91, 0x3c, 0x4b, 0x58,
|
||||
0x4c, 0xee, 0x41, 0x37, 0x48, 0x26, 0xfc, 0x74, 0x9b, 0x4b, 0xe6, 0x79, 0xe0, 0x3e, 0xe7, 0xb3,
|
||||
0xdc, 0x77, 0x06, 0xd6, 0xb0, 0x43, 0xf1, 0x4c, 0x7e, 0xb7, 0xe0, 0xe6, 0x97, 0x11, 0x8f, 0x27,
|
||||
0xdf, 0xa5, 0x32, 0x12, 0x49, 0xee, 0xbd, 0x07, 0xdd, 0x4d, 0x36, 0x3e, 0xe4, 0xbb, 0xb3, 0x94,
|
||||
0xa3, 0x65, 0x97, 0xd6, 0x40, 0xa5, 0x0d, 0xa3, 0xd7, 0xdc, 0x77, 0x07, 0xd6, 0xb0, 0x4f, 0x6b,
|
||||
0xc0, 0x1b, 0x40, 0x6f, 0x37, 0x9a, 0xf2, 0xef, 0x0b, 0x96, 0xc8, 0x62, 0xea, 0xb7, 0xf0, 0x76,
|
||||
0x13, 0x52, 0x14, 0xd0, 0x71, 0x07, 0x55, 0x78, 0xf6, 0x96, 0xc1, 0xd9, 0x8e, 0x12, 0xbf, 0x3b,
|
||||
0xb0, 0x86, 0x0e, 0x55, 0x47, 0x44, 0xd8, 0xa9, 0x0f, 0x06, 0x61, 0xa7, 0x15, 0xf5, 0x5e, 0x83,
|
||||
0x3a, 0x81, 0xa5, 0x60, 0x9a, 0x8a, 0x4c, 0x52, 0x9e, 0xa7, 0x22, 0xc9, 0xd1, 0xd3, 0x56, 0x96,
|
||||
0xf9, 0x16, 0x3a, 0x57, 0x47, 0xf2, 0x23, 0x2c, 0x6f, 0xc4, 0x62, 0x7c, 0x34, 0x62, 0x92, 0x51,
|
||||
0xfe, 0x43, 0xc1, 0x73, 0xe9, 0xbd, 0x0d, 0x2d, 0xcc, 0x89, 0xb1, 0xd3, 0x82, 0x42, 0x31, 0x0f,
|
||||
0xbe, 0xad, 0x51, 0x14, 0x14, 0x8a, 0xf7, 0x31, 0x13, 0x2e, 0xd5, 0x82, 0x42, 0xc3, 0x43, 0x96,
|
||||
0x4d, 0x30, 0x03, 0x2e, 0xd5, 0x82, 0xe2, 0xb8, 0x1f, 0xf1, 0x13, 0xf3, 0xd9, 0x78, 0x26, 0x01,
|
||||
0xac, 0x34, 0xe2, 0x1b, 0x9a, 0xab, 0xd0, 0xa6, 0xe2, 0x24, 0x18, 0xe5, 0xbe, 0x35, 0x70, 0x86,
|
||||
0x2e, 0x35, 0x12, 0x26, 0x57, 0xc4, 0xc5, 0x34, 0x51, 0x2a, 0x1b, 0x55, 0x35, 0x40, 0xee, 0x42,
|
||||
0x0b, 0x33, 0xad, 0xbe, 0xb2, 0xbe, 0xab, 0x8e, 0xe4, 0x27, 0x0b, 0xba, 0xdb, 0xec, 0x14, 0x69,
|
||||
0xe4, 0xde, 0x53, 0xe8, 0x84, 0x92, 0x25, 0x13, 0x45, 0x50, 0x19, 0xf5, 0x1e, 0xbf, 0xbf, 0x5e,
|
||||
0x36, 0xc4, 0x7a, 0x65, 0xb6, 0x5e, 0xda, 0x6c, 0x25, 0x32, 0x9b, 0xd1, 0xea, 0xca, 0xbb, 0x9f,
|
||||
0x41, 0xff, 0x9c, 0x4a, 0xc5, 0x3b, 0xe2, 0xb3, 0x32, 0xab, 0x47, 0x7c, 0xa6, 0xbe, 0xff, 0x98,
|
||||
0xc5, 0x05, 0xc7, 0x5c, 0xb9, 0x54, 0x0b, 0x9f, 0xda, 0x1f, 0x5b, 0x64, 0x1f, 0xbc, 0xcd, 0x8c,
|
||||
0x33, 0xc9, 0x31, 0xc8, 0x36, 0xcf, 0x73, 0xf6, 0x8a, 0x5f, 0x9e, 0x71, 0x9d, 0x45, 0xbb, 0x99,
|
||||
0xc5, 0xaa, 0x0e, 0x4e, 0xa3, 0x0e, 0xe4, 0x21, 0x78, 0x23, 0x1e, 0x73, 0xc9, 0x4d, 0x37, 0xff,
|
||||
0x8b, 0x5f, 0x12, 0x96, 0x1c, 0xae, 0xb6, 0xf5, 0x1e, 0x80, 0xab, 0x46, 0x03, 0x29, 0xf4, 0x1e,
|
||||
0xdf, 0xae, 0xf3, 0x54, 0x4d, 0x0d, 0x45, 0x03, 0x12, 0x97, 0x4e, 0x91, 0xcf, 0x95, 0x1f, 0xb6,
|
||||
0xa0, 0x95, 0x1e, 0x9a, 0x50, 0x0e, 0x86, 0x5a, 0xad, 0x43, 0x35, 0xc7, 0xcf, 0x44, 0x7b, 0x56,
|
||||
0x7e, 0xee, 0x75, 0xa3, 0x91, 0x97, 0x06, 0x55, 0x5d, 0xb9, 0xc3, 0xa6, 0xdc, 0xdc, 0xc1, 0x73,
|
||||
0x45, 0xc5, 0xbe, 0x9a, 0x8a, 0x72, 0xaf, 0x3a, 0x59, 0x6d, 0x0d, 0x47, 0xb9, 0x47, 0x81, 0x3c,
|
||||
0x81, 0x76, 0x38, 0x3e, 0xe4, 0x53, 0xe6, 0x7d, 0x00, 0x37, 0x90, 0x07, 0xcf, 0x4d, 0xb3, 0xdd,
|
||||
0x9a, 0x4b, 0x22, 0x2d, 0xf5, 0x64, 0x64, 0xf8, 0x2f, 0xe4, 0xf4, 0x00, 0xda, 0x18, 0x3d, 0xf7,
|
||||
0xdd, 0x79, 0x37, 0x88, 0x53, 0xa3, 0x26, 0x5b, 0xe0, 0xec, 0xd1, 0x40, 0x0d, 0x11, 0x32, 0x28,
|
||||
0xbd, 0x18, 0x49, 0xf9, 0xfe, 0x5a, 0xe4, 0xd2, 0x64, 0x03, 0xcf, 0x0a, 0x7b, 0x21, 0x32, 0x89,
|
||||
0xa9, 0xef, 0x53, 0x3c, 0x93, 0x97, 0xe0, 0xee, 0x88, 0x09, 0xf7, 0x96, 0xc0, 0x0e, 0x46, 0xc6,
|
||||
0x87, 0x1d, 0x8c, 0xbc, 0x7b, 0xe8, 0xde, 0xa4, 0xa6, 0x5f, 0x93, 0xd8, 0xa3, 0x01, 0xc5, 0xc0,
|
||||
0xf7, 0xa1, 0x1f, 0xe4, 0x9b, 0x42, 0x64, 0x93, 0x28, 0x61, 0x52, 0x64, 0x66, 0x9d, 0x9e, 0x07,
|
||||
0xc9, 0x33, 0x58, 0x56, 0xee, 0x43, 0xc9, 0x24, 0x2f, 0xeb, 0xb7, 0x0a, 0x6d, 0x85, 0x55, 0xe1,
|
||||
0x8c, 0x84, 0x83, 0xa0, 0xec, 0xca, 0x0a, 0xa2, 0x40, 0xbe, 0xd5, 0x1e, 0xb6, 0x8e, 0x79, 0x22,
|
||||
0x1b, 0x1d, 0x80, 0x32, 0x3a, 0xe8, 0x53, 0x2d, 0x78, 0x44, 0x7f, 0x8a, 0xe1, 0xbc, 0x54, 0x73,
|
||||
0x56, 0x28, 0x45, 0x1d, 0xf9, 0xc5, 0x02, 0x28, 0x09, 0x15, 0x79, 0x75, 0xc5, 0xba, 0xfc, 0x8a,
|
||||
0x37, 0x2c, 0x6b, 0x6c, 0x5a, 0x76, 0xb9, 0xb6, 0xd2, 0x38, 0x2d, 0x7b, 0xe0, 0xa3, 0xba, 0x07,
|
||||
0x74, 0xf1, 0xee, 0xcc, 0xf5, 0x80, 0x8e, 0x5a, 0x77, 0xc2, 0x0b, 0xe8, 0x35, 0xf0, 0x85, 0xfd,
|
||||
0xf0, 0x61, 0xd5, 0x0f, 0xf6, 0xbc, 0x4b, 0xc4, 0x8d, 0xcb, 0xb2, 0x2b, 0x9e, 0x43, 0xaf, 0x01,
|
||||
0x2f, 0xf4, 0x38, 0x84, 0x5b, 0x5f, 0x1c, 0xb3, 0x28, 0x66, 0x07, 0xb1, 0x5e, 0x4f, 0xe5, 0x92,
|
||||
0x9d, 0x87, 0x49, 0x04, 0xfd, 0xcd, 0xb8, 0xc8, 0x25, 0xcf, 0x8c, 0x3b, 0xb5, 0x99, 0x35, 0x50,
|
||||
0x15, 0xaf, 0x06, 0x16, 0xd7, 0xcf, 0xbb, 0x0f, 0x2d, 0x95, 0x46, 0x3d, 0x38, 0x17, 0x73, 0xac,
|
||||
0x95, 0x64, 0x1f, 0x3a, 0x1b, 0x61, 0xf0, 0x55, 0x26, 0x8a, 0x74, 0x21, 0xe9, 0xf2, 0xc1, 0xb4,
|
||||
0x2f, 0x3e, 0x98, 0xce, 0x85, 0x07, 0xd3, 0xad, 0x1e, 0x4c, 0x12, 0xc2, 0x8a, 0xde, 0x57, 0x6a,
|
||||
0x5e, 0xaf, 0xb3, 0xae, 0xca, 0xd7, 0xcc, 0x69, 0xbc, 0x66, 0x21, 0xac, 0xe8, 0xb5, 0xf4, 0x7f,
|
||||
0x3a, 0xfd, 0xcd, 0x86, 0x15, 0xca, 0xf3, 0xe8, 0x35, 0x0f, 0x92, 0x5c, 0x66, 0xc5, 0x58, 0x6d,
|
||||
0x1f, 0x75, 0xff, 0x1b, 0x71, 0x60, 0xb2, 0xed, 0x50, 0x2d, 0xbc, 0x49, 0xa7, 0x7b, 0x8f, 0xa0,
|
||||
0x37, 0x3f, 0x9d, 0x17, 0x4d, 0x9b, 0x26, 0xde, 0x23, 0xb8, 0x11, 0x8a, 0x22, 0x1b, 0x57, 0xed,
|
||||
0xdb, 0xd8, 0x88, 0x9a, 0x99, 0x56, 0xd3, 0xd2, 0xac, 0x31, 0x1a, 0xad, 0x2b, 0x46, 0xe3, 0xe9,
|
||||
0x5c, 0x2b, 0xf9, 0x6d, 0xbc, 0xf0, 0x4e, 0x7d, 0xe1, 0x9c, 0x9a, 0x9e, 0xb7, 0x26, 0x3f, 0x5b,
|
||||
0x70, 0xb3, 0x49, 0xe1, 0x8d, 0x06, 0xb7, 0xaa, 0x88, 0xbd, 0xb0, 0x22, 0xce, 0xa2, 0x8a, 0xb8,
|
||||
0x75, 0x45, 0xea, 0x87, 0xb9, 0xd5, 0x78, 0x98, 0xc9, 0x11, 0xdc, 0xbd, 0x50, 0xa6, 0x4d, 0x31,
|
||||
0x4d, 0x55, 0x3f, 0xfc, 0x87, 0x72, 0xa9, 0x95, 0x96, 0x65, 0xa6, 0x50, 0x5d, 0xaa, 0x05, 0xf2,
|
||||
0x09, 0xdc, 0x09, 0xb9, 0x6c, 0x14, 0xa9, 0xec, 0xb6, 0x01, 0x38, 0x3b, 0xfc, 0xe4, 0x92, 0xcf,
|
||||
0x57, 0x2a, 0xf2, 0x39, 0xf8, 0x7b, 0xe9, 0x84, 0x49, 0x7e, 0xad, 0xdb, 0x1b, 0xd0, 0xd9, 0x15,
|
||||
0xa9, 0x88, 0xc5, 0xab, 0xd9, 0x15, 0x53, 0xef, 0xc3, 0x0d, 0xbd, 0xbf, 0xf5, 0x1a, 0xe9, 0xd2,
|
||||
0x52, 0x24, 0xb7, 0x55, 0x43, 0x8f, 0x59, 0x3c, 0x2e, 0x62, 0x45, 0x43, 0xfd, 0xb4, 0xe5, 0x1b,
|
||||
0xcb, 0x7f, 0x9c, 0xad, 0x59, 0x7f, 0x9e, 0xad, 0x59, 0x7f, 0x9d, 0xad, 0x59, 0xbf, 0xfe, 0xbd,
|
||||
0xf6, 0xd6, 0x41, 0x1b, 0x7f, 0xd6, 0x9f, 0xfc, 0x13, 0x00, 0x00, 0xff, 0xff, 0x7e, 0x7c, 0x6d,
|
||||
0x22, 0xbd, 0x0b, 0x00, 0x00,
|
||||
// 1095 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x56, 0xdd, 0x6e, 0xdc, 0x44,
|
||||
0x14, 0xc6, 0x3f, 0xbb, 0xd9, 0x3d, 0xdb, 0x4d, 0x13, 0x97, 0x06, 0x17, 0xa1, 0x10, 0x46, 0x15,
|
||||
0x0d, 0x95, 0x08, 0x55, 0x7b, 0xc3, 0x5f, 0xa5, 0x92, 0x6c, 0x28, 0xa6, 0x24, 0x94, 0x71, 0x92,
|
||||
0xbb, 0x5e, 0x4c, 0x76, 0x47, 0x8d, 0x15, 0xaf, 0xc7, 0xd8, 0xe3, 0x24, 0xdb, 0x0b, 0x6e, 0x41,
|
||||
0xe2, 0x05, 0x10, 0x4f, 0xc2, 0x23, 0x70, 0xc9, 0x23, 0xa0, 0xf0, 0x22, 0x68, 0xce, 0x8c, 0x7f,
|
||||
0xb2, 0xd9, 0xb0, 0x55, 0xe8, 0xdd, 0x9c, 0xef, 0x9c, 0x39, 0xe7, 0x9b, 0xf3, 0x67, 0x43, 0x3f,
|
||||
0xcd, 0xa2, 0x13, 0x26, 0xf9, 0x46, 0x9a, 0x09, 0x29, 0xbc, 0x4e, 0x94, 0x48, 0x9e, 0x25, 0x2c,
|
||||
0x26, 0x4f, 0xa1, 0x1b, 0x24, 0x23, 0x7e, 0xb6, 0xc3, 0x25, 0xf3, 0x3c, 0x70, 0x9f, 0xf1, 0x49,
|
||||
0xee, 0x3b, 0x6b, 0xd6, 0x7a, 0x87, 0xe2, 0xd9, 0xfb, 0x10, 0x16, 0xf7, 0x32, 0x36, 0x3c, 0xde,
|
||||
0x3e, 0x8b, 0x72, 0xc9, 0x93, 0x21, 0xf7, 0x5d, 0xd4, 0x4e, 0xa1, 0xe4, 0x0f, 0x0b, 0x6e, 0x7c,
|
||||
0x1d, 0xf1, 0x78, 0xf4, 0x7d, 0x2a, 0x23, 0x91, 0xe4, 0xde, 0x7b, 0xd0, 0xdd, 0x62, 0xc3, 0x23,
|
||||
0xbe, 0x37, 0x49, 0x39, 0x7a, 0xec, 0xd2, 0x1a, 0xa8, 0xb4, 0x61, 0xf4, 0x4a, 0x7b, 0xec, 0xd3,
|
||||
0x1a, 0xf0, 0xd6, 0xa0, 0xb7, 0x17, 0x8d, 0xf9, 0x0f, 0x05, 0x4b, 0x64, 0x31, 0xf6, 0x5b, 0x78,
|
||||
0xbb, 0x09, 0x29, 0xaa, 0xe8, 0xb8, 0x83, 0x2a, 0x3c, 0x7b, 0x4b, 0xe0, 0xec, 0x44, 0x89, 0xdf,
|
||||
0x5d, 0xb3, 0xd6, 0x1d, 0xaa, 0x8e, 0x88, 0xb0, 0x33, 0x1f, 0x0c, 0xc2, 0xce, 0xaa, 0x27, 0xf6,
|
||||
0xea, 0x27, 0x12, 0x02, 0x8b, 0xc1, 0x38, 0x15, 0x99, 0xa4, 0x3c, 0x4f, 0x45, 0x92, 0xa3, 0xa7,
|
||||
0xed, 0x2c, 0xf3, 0x2d, 0x74, 0xae, 0x8e, 0xe4, 0x27, 0x58, 0xda, 0x8c, 0xc5, 0xf0, 0x78, 0xc0,
|
||||
0x24, 0xa3, 0xfc, 0xc7, 0x82, 0xe7, 0xd2, 0x7b, 0x1b, 0x5a, 0x98, 0x3b, 0x63, 0xa7, 0x05, 0x85,
|
||||
0x62, 0x1e, 0x7c, 0x5b, 0xa3, 0x28, 0x28, 0x14, 0xef, 0x63, 0x26, 0x5c, 0xaa, 0x05, 0x85, 0x86,
|
||||
0x47, 0x2c, 0x1b, 0x61, 0x06, 0x5c, 0xaa, 0x05, 0xc5, 0xf1, 0x20, 0xe2, 0xa7, 0xe6, 0xd9, 0x78,
|
||||
0x26, 0x01, 0x2c, 0x37, 0xe2, 0x1b, 0x9a, 0x2b, 0xd0, 0xa6, 0xe2, 0x34, 0x18, 0xe4, 0xbe, 0xb5,
|
||||
0xe6, 0xac, 0xbb, 0xd4, 0x48, 0x98, 0x5c, 0x11, 0x17, 0xe3, 0x44, 0xa9, 0x6c, 0x54, 0xd5, 0x00,
|
||||
0xb9, 0x03, 0x2d, 0xcc, 0xb4, 0x7a, 0x65, 0x7d, 0x57, 0x1d, 0xc9, 0xcf, 0x16, 0x74, 0x77, 0xd8,
|
||||
0x19, 0xd2, 0xc8, 0xbd, 0xc7, 0xd0, 0x09, 0x25, 0x4b, 0x46, 0x8a, 0xa0, 0x32, 0xea, 0x3d, 0xfc,
|
||||
0x60, 0xa3, 0x6c, 0x9c, 0x8d, 0xca, 0x6c, 0xa3, 0xb4, 0xd9, 0x4e, 0x64, 0x36, 0xa1, 0xd5, 0x95,
|
||||
0x77, 0xbf, 0x80, 0xfe, 0x05, 0x95, 0x8a, 0x77, 0xcc, 0x27, 0x65, 0x56, 0x8f, 0xf9, 0x44, 0xbd,
|
||||
0xff, 0x84, 0xc5, 0x05, 0xc7, 0x5c, 0xb9, 0x54, 0x0b, 0x9f, 0xdb, 0x9f, 0x5a, 0xe4, 0x00, 0xbc,
|
||||
0xad, 0x8c, 0x33, 0xc9, 0x31, 0xc8, 0x0e, 0xcf, 0x73, 0xf6, 0x92, 0x5f, 0x9d, 0x71, 0x9d, 0x45,
|
||||
0xbb, 0x99, 0xc5, 0xaa, 0x0e, 0x4e, 0xa3, 0x0e, 0xe4, 0x3e, 0x78, 0x03, 0x1e, 0x73, 0xc9, 0x4d,
|
||||
0xd7, 0xff, 0x87, 0x5f, 0x12, 0x96, 0x1c, 0xe6, 0xdb, 0x7a, 0xf7, 0xc0, 0x55, 0x23, 0x84, 0x14,
|
||||
0x7a, 0x0f, 0x6f, 0xd5, 0x79, 0xaa, 0xa6, 0x8b, 0xa2, 0x01, 0x89, 0x4b, 0xa7, 0xc8, 0x67, 0xee,
|
||||
0xc3, 0x66, 0xb4, 0xd2, 0x7d, 0x13, 0xca, 0xc1, 0x50, 0x2b, 0x75, 0xa8, 0xe6, 0xf8, 0x99, 0x68,
|
||||
0x4f, 0xca, 0xe7, 0x5e, 0x37, 0x1a, 0x79, 0x61, 0x50, 0xd5, 0x95, 0xbb, 0x6c, 0xcc, 0xcd, 0x1d,
|
||||
0x3c, 0x57, 0x54, 0xec, 0xf9, 0x54, 0x94, 0x7b, 0xd5, 0xc9, 0x6a, 0xbb, 0x38, 0xca, 0x3d, 0x0a,
|
||||
0xe4, 0x11, 0xb4, 0xc3, 0xe1, 0x11, 0x1f, 0x33, 0xef, 0x23, 0x58, 0x40, 0x1e, 0x3c, 0x37, 0xcd,
|
||||
0x76, 0x73, 0x2a, 0x89, 0xb4, 0xd4, 0x93, 0x81, 0xe1, 0x3f, 0x93, 0xd3, 0x3d, 0x68, 0x63, 0xf4,
|
||||
0xdc, 0x77, 0xa7, 0xdd, 0x20, 0x4e, 0x8d, 0x9a, 0x6c, 0x83, 0xb3, 0x4f, 0x03, 0x35, 0x44, 0xc8,
|
||||
0xa0, 0xf4, 0x62, 0x24, 0xe5, 0xfb, 0x1b, 0x91, 0x4b, 0x93, 0x0d, 0x3c, 0x2b, 0xec, 0xb9, 0xc8,
|
||||
0x24, 0xa6, 0xbe, 0x4f, 0xf1, 0x4c, 0x5e, 0x80, 0xbb, 0x2b, 0x46, 0xdc, 0x5b, 0x04, 0x3b, 0x18,
|
||||
0x18, 0x1f, 0x76, 0x30, 0xf0, 0xde, 0x47, 0xf7, 0x26, 0x35, 0xfd, 0x9a, 0xc4, 0x3e, 0x0d, 0x28,
|
||||
0x06, 0xbe, 0x0b, 0xfd, 0x20, 0xdf, 0x12, 0x22, 0x1b, 0x45, 0x09, 0x93, 0x22, 0x33, 0x6b, 0xf7,
|
||||
0x22, 0x48, 0x9e, 0xc0, 0x92, 0x72, 0x1f, 0x4a, 0x26, 0x79, 0x59, 0xbf, 0x15, 0x68, 0x2b, 0xac,
|
||||
0x0a, 0x67, 0x24, 0x1c, 0x04, 0x65, 0x57, 0x56, 0x10, 0x05, 0xf2, 0x9d, 0xf6, 0xb0, 0x7d, 0xc2,
|
||||
0x13, 0xd9, 0xe8, 0x00, 0x94, 0xd1, 0x41, 0x9f, 0x6a, 0xc1, 0x23, 0xfa, 0x29, 0x86, 0xf3, 0x62,
|
||||
0xcd, 0x59, 0xa1, 0x14, 0x75, 0xe4, 0x57, 0x0b, 0xa0, 0x24, 0x54, 0xe4, 0xd5, 0x15, 0xeb, 0xea,
|
||||
0x2b, 0xde, 0x7a, 0x59, 0x63, 0xd3, 0xb2, 0x4b, 0xb5, 0x95, 0xc6, 0x69, 0xd9, 0x03, 0x9f, 0xd4,
|
||||
0x3d, 0xa0, 0x8b, 0x77, 0x7b, 0xaa, 0x07, 0x74, 0xd4, 0xba, 0x13, 0x9e, 0x43, 0xaf, 0x81, 0xcf,
|
||||
0xec, 0x87, 0x8f, 0xab, 0x7e, 0xb0, 0xa7, 0x5d, 0x22, 0x6e, 0x5c, 0x96, 0x5d, 0xf1, 0x0c, 0x7a,
|
||||
0x0d, 0x78, 0xa6, 0xc7, 0x75, 0xb8, 0xf9, 0xd5, 0x09, 0x8b, 0x62, 0x76, 0x18, 0xeb, 0xf5, 0x54,
|
||||
0x2e, 0xd9, 0x69, 0x98, 0x44, 0xd0, 0xdf, 0x8a, 0x8b, 0x5c, 0xf2, 0xcc, 0xb8, 0x53, 0x9b, 0x59,
|
||||
0x03, 0x55, 0xf1, 0x6a, 0x60, 0x76, 0xfd, 0xbc, 0xbb, 0xd0, 0x52, 0x69, 0xd4, 0x83, 0x73, 0x39,
|
||||
0xc7, 0x5a, 0x49, 0x0e, 0xa0, 0xb3, 0x19, 0x06, 0x4f, 0x33, 0x51, 0xa4, 0x33, 0x49, 0x97, 0x1f,
|
||||
0x4c, 0xfb, 0xf2, 0x07, 0xd3, 0xb9, 0xf4, 0xc1, 0x74, 0xab, 0x0f, 0x26, 0x09, 0x61, 0x59, 0xef,
|
||||
0x2b, 0x35, 0xaf, 0xd7, 0x59, 0x57, 0xe5, 0xd7, 0xcc, 0x69, 0x7c, 0xcd, 0x42, 0x58, 0xd6, 0x6b,
|
||||
0xe9, 0x4d, 0x3a, 0xfd, 0xdd, 0x86, 0x65, 0xca, 0xf3, 0xe8, 0x15, 0x0f, 0x92, 0x5c, 0x66, 0xc5,
|
||||
0x50, 0x6d, 0x1f, 0x75, 0xff, 0x5b, 0x71, 0x68, 0xb2, 0xed, 0x50, 0x2d, 0xbc, 0x4e, 0xa7, 0x7b,
|
||||
0x0f, 0xa0, 0x37, 0x3d, 0x9d, 0x97, 0x4d, 0x9b, 0x26, 0xde, 0x03, 0x58, 0x08, 0x45, 0x91, 0x0d,
|
||||
0xab, 0xf6, 0x6d, 0x6c, 0x44, 0xcd, 0x4c, 0xab, 0x69, 0x69, 0xd6, 0x18, 0x8d, 0xd6, 0x9c, 0xd1,
|
||||
0x78, 0x3c, 0xd5, 0x4a, 0x7e, 0x1b, 0x2f, 0xbc, 0x53, 0x5f, 0xb8, 0xa0, 0xa6, 0x17, 0xad, 0xc9,
|
||||
0x2f, 0x16, 0xdc, 0x68, 0x52, 0x78, 0xad, 0xc1, 0xad, 0x2a, 0x62, 0xcf, 0xac, 0x88, 0x33, 0xab,
|
||||
0x22, 0x6e, 0x5d, 0x91, 0xfa, 0xc3, 0xdc, 0x6a, 0x7c, 0x98, 0xc9, 0x31, 0xdc, 0xb9, 0x54, 0xa6,
|
||||
0x2d, 0x31, 0x4e, 0x55, 0x3f, 0xfc, 0x8f, 0x72, 0xa9, 0x95, 0x96, 0x65, 0xa6, 0x50, 0x5d, 0xaa,
|
||||
0x05, 0xf2, 0x19, 0xdc, 0x0e, 0xb9, 0x6c, 0x14, 0xa9, 0xec, 0xb6, 0x35, 0x70, 0x76, 0xf9, 0xe9,
|
||||
0x15, 0xcf, 0x57, 0x2a, 0xf2, 0x25, 0xf8, 0xfb, 0xe9, 0x88, 0x49, 0x7e, 0xad, 0xdb, 0x9b, 0xd0,
|
||||
0xd9, 0x13, 0xa9, 0x88, 0xc5, 0xcb, 0xc9, 0x9c, 0xa9, 0xf7, 0x61, 0x41, 0xef, 0x6f, 0xbd, 0x46,
|
||||
0xba, 0xb4, 0x14, 0xc9, 0x2d, 0xd5, 0xd0, 0x43, 0x16, 0x0f, 0x8b, 0x58, 0xd1, 0x50, 0x3f, 0x6d,
|
||||
0xf9, 0xe6, 0xd2, 0x9f, 0xe7, 0xab, 0xd6, 0x5f, 0xe7, 0xab, 0xd6, 0xdf, 0xe7, 0xab, 0xd6, 0x6f,
|
||||
0xff, 0xac, 0xbe, 0x75, 0xd8, 0xc6, 0x9f, 0xfa, 0x47, 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0x31,
|
||||
0x07, 0xf3, 0xac, 0xe5, 0x0b, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ package internal;
|
|||
|
||||
message IndexMeta {
|
||||
bool Keys = 3;
|
||||
bool TrackExistence = 4;
|
||||
}
|
||||
|
||||
message FieldOptions {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,17 @@
|
|||
// Copyright 2017 Pilosa Corp.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
|
|
@ -95,11 +109,17 @@ func Difference(args ...*pql.Call) *pql.Call {
|
|||
kvargs, children := magic(args)
|
||||
return &pql.Call{Name: "Difference", Args: kvargs, Children: children}
|
||||
}
|
||||
|
||||
func Xor(args ...*pql.Call) *pql.Call {
|
||||
kvargs, children := magic(args)
|
||||
return &pql.Call{Name: "Xor", Args: kvargs, Children: children}
|
||||
}
|
||||
|
||||
func Not(args ...*pql.Call) *pql.Call {
|
||||
kvargs, children := magic(args)
|
||||
return &pql.Call{Name: "Not", Args: kvargs, Children: children}
|
||||
}
|
||||
|
||||
func Between(_ string, min, max int) *pql.Call {
|
||||
return &pql.Call{
|
||||
Name: "Range",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,17 @@
|
|||
// Copyright 2017 Pilosa Corp.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
|
|
@ -208,6 +222,24 @@ func TestPQL_Generator(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
pql: `Not(Row(aaa=10))`,
|
||||
calc: PQL(Not(Row("aaa", 10))),
|
||||
exp: &pql.Query{
|
||||
Calls: []*pql.Call{
|
||||
{
|
||||
Name: "Not",
|
||||
Args: map[string]interface{}{},
|
||||
Children: []*pql.Call{
|
||||
{
|
||||
Name: "Row",
|
||||
Args: map[string]interface{}{"frame": "aaa", "row": 10},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
|
||||
if !Compare(u.calc, u.exp) {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ var (
|
|||
ErrName = errors.New("invalid index or field name, must match [a-z0-9_-]")
|
||||
ErrLabel = errors.New("invalid row or column label, must match [A-Za-z0-9_-]")
|
||||
|
||||
ErrReservedName = errors.New("reserved index or field name")
|
||||
|
||||
// ErrFragmentNotFound is returned when a fragment does not exist.
|
||||
ErrFragmentNotFound = errors.New("fragment not found")
|
||||
ErrQueryRequired = errors.New("query required")
|
||||
|
|
@ -125,6 +127,9 @@ const TimeFormat = "2006-01-02T15:04"
|
|||
|
||||
// validateName ensures that the name is a valid format.
|
||||
func validateName(name string) error {
|
||||
if name == existenceFieldName {
|
||||
return ErrReservedName
|
||||
}
|
||||
if !nameRegexp.Match([]byte(name)) {
|
||||
return ErrName
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ func TestValidateNameInvalid(t *testing.T) {
|
|||
names := []string{
|
||||
"", "'", "^", "/", "\\", "A", "*", "a:b", "valid?no", "yüce", "1", "_", "-",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1",
|
||||
"exists",
|
||||
}
|
||||
for _, name := range names {
|
||||
if validateName(name) == nil {
|
||||
|
|
|
|||
|
|
@ -595,6 +595,23 @@ func TestPQLDeepEquality(t *testing.T) {
|
|||
{Name: "Row"},
|
||||
},
|
||||
}},
|
||||
{
|
||||
name: "OptionsWrapper",
|
||||
call: "Options(Row(f1=123), excludeRowAttrs=true)",
|
||||
exp: &Call{
|
||||
Name: "Options",
|
||||
Args: map[string]interface{}{
|
||||
"excludeRowAttrs": true,
|
||||
},
|
||||
Children: []*Call{
|
||||
{
|
||||
Name: "Row",
|
||||
Args: map[string]interface{}{
|
||||
"f1": int64(123),
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@ package roaring
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"math/bits"
|
||||
"sort"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -614,8 +615,9 @@ func (b *Bitmap) WriteTo(w io.Writer) (n int64, err error) {
|
|||
return n, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary decodes b from a binary-encoded byte slice.
|
||||
func (b *Bitmap) UnmarshalBinary(data []byte) error {
|
||||
// unmarshalPilosaRoaring treats data as being encoded in Pilosa's 64 bit
|
||||
// roaring format and decodes it into b.
|
||||
func (b *Bitmap) unmarshalPilosaRoaring(data []byte) error {
|
||||
if len(data) < headerBaseSize {
|
||||
return errors.New("data too small")
|
||||
}
|
||||
|
|
@ -3397,3 +3399,164 @@ func popcountAndSlice(s, m []uint64) uint64 {
|
|||
}
|
||||
return cnt
|
||||
}
|
||||
|
||||
// constants from github.com/RoaringBitmap/roaring
|
||||
// taken from roaring/util.go
|
||||
const (
|
||||
serialCookieNoRunContainer = 12346 // only arrays and bitmaps
|
||||
serialCookie = 12347 // runs, arrays, and bitmaps
|
||||
)
|
||||
|
||||
func readOfficialHeader(buf []byte) (size uint32, containerTyper func(index uint, card int) byte, header, pos int, haveRuns bool, err error) {
|
||||
if len(buf) < 8 {
|
||||
err = fmt.Errorf("buffer too small, expecting at least 8 bytes, was %d", len(buf))
|
||||
return size, containerTyper, header, pos, haveRuns, err
|
||||
}
|
||||
cf := func(index uint, card int) (newType byte) {
|
||||
newType = containerBitmap
|
||||
if card < ArrayMaxSize {
|
||||
newType = containerArray
|
||||
}
|
||||
return newType
|
||||
}
|
||||
containerTyper = cf
|
||||
cookie := binary.LittleEndian.Uint32(buf)
|
||||
pos += 4
|
||||
|
||||
// cookie header
|
||||
if cookie == serialCookieNoRunContainer {
|
||||
size = binary.LittleEndian.Uint32(buf[pos:])
|
||||
pos += 4
|
||||
} else if cookie&0x0000FFFF == serialCookie {
|
||||
haveRuns = true
|
||||
size = uint32(uint16(cookie>>16) + 1) // number of containers
|
||||
|
||||
// create is-run-container bitmap
|
||||
isRunBitmapSize := (int(size) + 7) / 8
|
||||
if pos+isRunBitmapSize > len(buf) {
|
||||
err = fmt.Errorf("malformed bitmap, is-run bitmap overruns buffer at %d", pos+isRunBitmapSize)
|
||||
return size, containerTyper, header, pos, haveRuns, err
|
||||
}
|
||||
|
||||
isRunBitmap := buf[pos : pos+isRunBitmapSize]
|
||||
pos += isRunBitmapSize
|
||||
containerTyper = func(index uint, card int) byte {
|
||||
if isRunBitmap[index/8]&(1<<(index%8)) != 0 {
|
||||
return containerRun
|
||||
}
|
||||
return cf(index, card)
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf("did not find expected serialCookie in header")
|
||||
return size, containerTyper, header, pos, haveRuns, err
|
||||
}
|
||||
|
||||
header = pos
|
||||
if size > (1 << 16) {
|
||||
err = fmt.Errorf("It is logically impossible to have more than (1<<16) containers.")
|
||||
return size, containerTyper, header, pos, haveRuns, err
|
||||
}
|
||||
|
||||
// descriptive header
|
||||
if pos+2*2*int(size) > len(buf) {
|
||||
err = fmt.Errorf("malformed bitmap, key-cardinality slice overruns buffer at %d", pos+2*2*int(size))
|
||||
return size, containerTyper, header, pos, haveRuns, err
|
||||
}
|
||||
pos += 2 * 2 * int(size) // moving pos past keycount
|
||||
return size, containerTyper, header, pos, haveRuns, err
|
||||
}
|
||||
|
||||
// UnmarshalBinary decodes b from a binary-encoded byte slice. data can be in
|
||||
// either official roaring format or Pilosa's roaring format.
|
||||
func (b *Bitmap) UnmarshalBinary(data []byte) error {
|
||||
fileMagic := uint32(binary.LittleEndian.Uint16(data[0:2]))
|
||||
if fileMagic == magicNumber { // if pilosa roaring
|
||||
return errors.Wrap(b.unmarshalPilosaRoaring(data), "unmarshaling as pilosa roaring")
|
||||
}
|
||||
|
||||
keyN, containerTyper, header, pos, haveRuns, err := readOfficialHeader(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading roaring header")
|
||||
}
|
||||
|
||||
b.Containers.Reset()
|
||||
// Descriptive header section: Read container keys and cardinalities.
|
||||
for i, buf := uint(0), data[header:]; i < uint(keyN); i, buf = i+1, buf[4:] {
|
||||
card := int(binary.LittleEndian.Uint16(buf[2:4])) + 1
|
||||
b.Containers.PutContainerValues(
|
||||
uint64(binary.LittleEndian.Uint16(buf[0:2])),
|
||||
containerTyper(i, card), /// container type voodo with isRunBitmap
|
||||
card,
|
||||
true)
|
||||
}
|
||||
|
||||
// Read container offsets and attach data.
|
||||
if haveRuns {
|
||||
readWithRuns(b, data, pos, keyN)
|
||||
} else {
|
||||
err := readOffsets(b, data, pos, keyN)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading offsets from official roaring format")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readOffsets(b *Bitmap, data []byte, pos int, keyN uint32) error {
|
||||
|
||||
citer, _ := b.Containers.Iterator(0)
|
||||
for i, buf := 0, data[pos:]; i < int(keyN); i, buf = i+1, buf[4:] {
|
||||
offset := binary.LittleEndian.Uint32(buf[0:4])
|
||||
// Verify the offset is within the bounds of the input data.
|
||||
if int(offset) >= len(data) {
|
||||
return fmt.Errorf("offset out of bounds: off=%d, len=%d", offset, len(data))
|
||||
}
|
||||
|
||||
// Map byte slice directly to the container data.
|
||||
citer.Next()
|
||||
_, c := citer.Value()
|
||||
switch c.containerType {
|
||||
case containerArray:
|
||||
c.runs = nil
|
||||
c.bitmap = nil
|
||||
c.array = (*[0xFFFFFFF]uint16)(unsafe.Pointer(&data[offset]))[:c.n]
|
||||
case containerBitmap:
|
||||
c.array = nil
|
||||
c.runs = nil
|
||||
c.bitmap = (*[0xFFFFFFF]uint64)(unsafe.Pointer(&data[offset]))[:bitmapN]
|
||||
default:
|
||||
return fmt.Errorf("unsupported container type %d", c.containerType)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readWithRuns(b *Bitmap, data []byte, pos int, keyN uint32) {
|
||||
citer, _ := b.Containers.Iterator(0)
|
||||
for i := 0; i < int(keyN); i++ {
|
||||
citer.Next()
|
||||
_, c := citer.Value()
|
||||
switch c.containerType {
|
||||
case containerRun:
|
||||
c.array = nil
|
||||
c.bitmap = nil
|
||||
runCount := binary.LittleEndian.Uint16(data[pos : pos+runCountHeaderSize])
|
||||
c.runs = (*[0xFFFFFFF]interval16)(unsafe.Pointer(&data[pos+runCountHeaderSize]))[:runCount]
|
||||
|
||||
for o := range c.runs { // must convert from start:length to start:end :(
|
||||
c.runs[o].last = c.runs[o].start + c.runs[o].last
|
||||
}
|
||||
pos += int((runCount * interval16Size) + runCountHeaderSize)
|
||||
case containerArray:
|
||||
c.runs = nil
|
||||
c.bitmap = nil
|
||||
c.array = (*[0xFFFFFFF]uint16)(unsafe.Pointer(&data[pos]))[:c.n]
|
||||
pos += c.n * 2
|
||||
case containerBitmap:
|
||||
c.array = nil
|
||||
c.runs = nil
|
||||
c.bitmap = (*[0xFFFFFFF]uint64)(unsafe.Pointer(&data[pos]))[:bitmapN]
|
||||
pos += bitmapN * 8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ package roaring
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
|
@ -3225,3 +3227,37 @@ func runContainerFunc(f interface{}, c ...*Container) *Container {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestUnmarshalOfficialRoaring(t *testing.T) {
|
||||
//generated serialize image from java(clojure) with arrays
|
||||
rbContainerWithTwoArrays, _ := hex.DecodeString("3A300000020000000000020001000000180000001E0000000100020003000100")
|
||||
bm := NewBitmap()
|
||||
er := bm.UnmarshalBinary(rbContainerWithTwoArrays)
|
||||
if er != nil {
|
||||
t.Fatalf("UnmarshalOfficialRoaring %s", er)
|
||||
}
|
||||
if bm.Count() != 4 {
|
||||
t.Fatalf("unexpected bitmap %v expected bits [1 2 3 65537]", bm.Slice())
|
||||
}
|
||||
//generated serialize image from java(clojure) with a run and array
|
||||
rbContainerWithRLEandArray, _ := hex.DecodeString("3B3001000100000900010000000100010009000100")
|
||||
bm = NewBitmap()
|
||||
er = bm.UnmarshalBinary(rbContainerWithRLEandArray)
|
||||
if er != nil {
|
||||
t.Fatalf("UnmarshalOfficialRoaring %s", er)
|
||||
}
|
||||
if bm.Count() != 11 {
|
||||
t.Fatalf("unexpected bitmap %v expected bits [1 2 3 4 5 6 7 8 9 10 65537]", bm.Slice())
|
||||
}
|
||||
//had to use an external file because emacs was barfing on the long line :()
|
||||
_bitmap_array_container, _ := ioutil.ReadFile("testdata/bitmapcontainer.roaringbitmap")
|
||||
bm = NewBitmap()
|
||||
er = bm.UnmarshalBinary(_bitmap_array_container)
|
||||
if er != nil {
|
||||
t.Fatalf("UnmarshalOfficialRoaring %s", er)
|
||||
}
|
||||
if bm.Count() != 10000 {
|
||||
t.Fatalf("expecting X got %d", bm.Count())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
BIN
roaring/testdata/bitmapcontainer.roaringbitmap
vendored
Normal file
BIN
roaring/testdata/bitmapcontainer.roaringbitmap
vendored
Normal file
Binary file not shown.
|
|
@ -16,6 +16,8 @@ package server_test
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -86,6 +88,22 @@ func TestHandler_Endpoints(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("ImportRoaring", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
roaringData, _ := hex.DecodeString("3B3001000100000900010000000100010009000100")
|
||||
req := test.MustNewHTTPRequest("POST", "/index/i0/field/f1/import-roaring/0", bytes.NewBuffer(roaringData))
|
||||
req.Header.Set("Content-Type", "application/x-binary")
|
||||
h.ServeHTTP(w, req)
|
||||
resp, err := cmd.API.Query(context.Background(), &pilosa.QueryRequest{Index: "i0", Query: "TopN(f1)"})
|
||||
if err != nil {
|
||||
t.Fatalf("querying: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(resp.Results[0], []pilosa.Pair{{Count: 12, ID: 0}}) {
|
||||
t.Fatalf("Unexpected result %v", resp.Results[0])
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
t.Run("Status", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/status", nil))
|
||||
|
|
@ -568,7 +586,8 @@ func TestHandler_Endpoints(t *testing.T) {
|
|||
if result.StatusCode != 200 {
|
||||
t.Fatalf("CORS preflight status should be 200, but is %v", result.StatusCode)
|
||||
}
|
||||
if w.HeaderMap["Access-Control-Allow-Origin"][0] != "http://test/" {
|
||||
|
||||
if result.Header["Access-Control-Allow-Origin"][0] != "http://test/" {
|
||||
t.Fatal("CORS header not present")
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -91,6 +91,20 @@ func (h *Holder) Row(index, field string, rowID uint64) *pilosa.Row {
|
|||
return row
|
||||
}
|
||||
|
||||
// ReadRow returns a Row for a given field. If the field does not exist,
|
||||
// it panics rather than creating the field.
|
||||
func (h *Holder) ReadRow(index, field string, rowID uint64) *pilosa.Row {
|
||||
f := h.Holder.Field(index, field)
|
||||
if f == nil {
|
||||
panic(pilosa.ErrFieldNotFound)
|
||||
}
|
||||
row, err := f.Row(rowID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func (h *Holder) RowAttrStore(index, field string) pilosa.AttrStore {
|
||||
idx := h.MustCreateIndexIfNotExists(index, pilosa.IndexOptions{})
|
||||
f, err := idx.CreateFieldIfNotExists(field, pilosa.OptFieldTypeDefault())
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
gohttp "net/http"
|
||||
"os"
|
||||
|
|
@ -40,10 +39,6 @@ type Command struct {
|
|||
*server.Command
|
||||
|
||||
commandOptions []server.CommandOption
|
||||
|
||||
stdin bytes.Buffer
|
||||
stdout bytes.Buffer
|
||||
stderr bytes.Buffer
|
||||
}
|
||||
|
||||
func OptAllowedOrigins(origins []string) server.CommandOption {
|
||||
|
|
@ -65,17 +60,15 @@ func newCommand(opts ...server.CommandOption) *Command {
|
|||
// beginning of the option slice so that it can be overridden by user-passed
|
||||
// options.
|
||||
opts = append([]server.CommandOption{server.OptCommandCloseTimeout(time.Millisecond * 2)}, opts...)
|
||||
m := &Command{Command: server.NewCommand(os.Stdin, os.Stdout, os.Stderr, opts...), commandOptions: opts}
|
||||
m := &Command{commandOptions: opts}
|
||||
m.Command = server.NewCommand(bytes.NewReader(nil), ioutil.Discard, ioutil.Discard, opts...)
|
||||
m.Config.DataDir = path
|
||||
m.Config.Bind = "http://localhost:0"
|
||||
m.Config.Cluster.Disabled = true
|
||||
m.Command.Stdin = &m.stdin
|
||||
m.Command.Stdout = &m.stdout
|
||||
m.Command.Stderr = &m.stderr
|
||||
|
||||
if testing.Verbose() {
|
||||
m.Command.Stdout = io.MultiWriter(os.Stdout, m.Command.Stdout)
|
||||
m.Command.Stderr = io.MultiWriter(os.Stderr, m.Command.Stderr)
|
||||
m.Command.Stdout = os.Stdout
|
||||
m.Command.Stderr = os.Stderr
|
||||
}
|
||||
|
||||
return m
|
||||
|
|
@ -120,7 +113,7 @@ func (m *Command) Reopen() error {
|
|||
|
||||
// Create new main with the same config.
|
||||
config := m.Command.Config
|
||||
m.Command = server.NewCommand(os.Stdin, os.Stdout, os.Stderr, m.commandOptions...)
|
||||
m.Command = server.NewCommand(bytes.NewReader(nil), ioutil.Discard, ioutil.Discard, m.commandOptions...)
|
||||
m.Command.Config = config
|
||||
|
||||
// Run new program.
|
||||
|
|
@ -129,40 +122,40 @@ func (m *Command) Reopen() error {
|
|||
|
||||
// MustCreateIndex uses this command's API to create an index and fails the test
|
||||
// if there is an error.
|
||||
func (m *Command) MustCreateIndex(t *testing.T, name string, opts pilosa.IndexOptions) *pilosa.Index {
|
||||
func (m *Command) MustCreateIndex(tb testing.TB, name string, opts pilosa.IndexOptions) *pilosa.Index {
|
||||
idx, err := m.API.CreateIndex(context.Background(), name, opts)
|
||||
if err != nil {
|
||||
t.Fatalf("creating index: %v with options: %v, err: %v", name, opts, err)
|
||||
tb.Fatalf("creating index: %v with options: %v, err: %v", name, opts, err)
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
// MustCreateField uses this command's API to create the field. The index must
|
||||
// already exist - it fails the test if there is an error.
|
||||
func (m *Command) MustCreateField(t *testing.T, index, field string, opts ...pilosa.FieldOption) *pilosa.Field {
|
||||
func (m *Command) MustCreateField(tb testing.TB, index, field string, opts ...pilosa.FieldOption) *pilosa.Field {
|
||||
f, err := m.API.CreateField(context.Background(), index, field, opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("creating field: %s in index: %s err: %v", field, index, err)
|
||||
tb.Fatalf("creating field: %s in index: %s err: %v", field, index, err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// MustQuery uses this command's API to execute the given query request, failing
|
||||
// if Query returns a non-nil error, otherwise returning the QueryResponse.
|
||||
func (m *Command) MustQuery(t *testing.T, req *pilosa.QueryRequest) pilosa.QueryResponse {
|
||||
func (m *Command) MustQuery(tb testing.TB, req *pilosa.QueryRequest) pilosa.QueryResponse {
|
||||
resp, err := m.API.Query(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("making query: %v, err: %v", req, err)
|
||||
tb.Fatalf("making query: %v, err: %v", req, err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// MustRecalculateCaches calls RecalculateCaches on the command's API, and fails
|
||||
// if there is an error.
|
||||
func (m *Command) MustRecalculateCaches(t *testing.T) {
|
||||
func (m *Command) MustRecalculateCaches(tb testing.TB) {
|
||||
err := m.API.RecalculateCaches(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("recalcluating caches: %v", err)
|
||||
tb.Fatalf("recalcluating caches: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -224,10 +217,10 @@ func (c Cluster) Close() error {
|
|||
}
|
||||
|
||||
// MustNewCluster creates a new cluster
|
||||
func MustNewCluster(t *testing.T, size int, opts ...[]server.CommandOption) Cluster {
|
||||
func MustNewCluster(tb testing.TB, size int, opts ...[]server.CommandOption) Cluster {
|
||||
c, err := newCluster(size, opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("new cluster: %v", err)
|
||||
tb.Fatalf("new cluster: %v", err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
|
@ -271,10 +264,10 @@ func runCluster(size int, opts ...[]server.CommandOption) (Cluster, error) {
|
|||
}
|
||||
|
||||
// MustRunCluster creates and starts a new cluster
|
||||
func MustRunCluster(t *testing.T, size int, opts ...[]server.CommandOption) Cluster {
|
||||
func MustRunCluster(tb testing.TB, size int, opts ...[]server.CommandOption) Cluster {
|
||||
c, err := runCluster(size, opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("run cluster: %v", err)
|
||||
tb.Fatalf("run cluster: %v", err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue