mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
When doing tests, we create a ton of one-off clusters. This turns out to be expensive and slow. Fixing it is surprisingly hard. Fundamentally: If we're sharing clusters, we need to use different indexes for each test, to avoid clashes. This changes index names. As a side-effect, this reorders many partition-based things, like the order keys are returned in. Thus, to fix this, we change a lot of tests to no longer depend on the *order* in which strings are returned. Having done that, we can also discard the ModHasher behavior, since that only existed to allow us to reliably predict partitioning. The basic design is as follows: Instead of a cluster being a []*Command, a "shareable" cluster is now a []*Command plus some flags, and a "cluster" is a pointer to a possibly-shared cluster, plus a link to the specific test using this specific cluster, and correspondingly, its test name suitably coerced to be a valid index name prefix. The "test.Cluster" object now has methods to allow retrieving an index name, and also implemnts fmt.Formatter to let you use, e.g., `%i` with it in Sprintf to get "the index name, plus an i". (This works for everything but %p and %T.) This allows us to consistently rework all the many things that use index names in a persistent way. We also have `MustUnshared` and `MustRunUnsharedCluster` methods which allow us to specify that a given test needs its own cluster for some reason. For instance, the tests that want to run backups need their own isolated cluster, and the tests that want to close or reopen nodes need their own cluster because a reopened cluster won't have working GRPC for some reason. On "closing" a shared cluster (actually the test-specific wrapper that reflects a given sharing), we delete any indexes starting with that test's index name prefix. Otherwise, the huge pile of open indexes prevents `go test -race` from working on MacOS, where we run out of address space too quickly. This is fairly enormous but most of the individual changes are fairly trivial things like replacing the string "i" with "c.Idx()". We also tweaked a test that failed for me a couple of times to not depend on sort order.
1441 lines
48 KiB
Go
1441 lines
48 KiB
Go
// Copyright 2022 Molecula Corp. (DBA FeatureBase).
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
package server_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
gohttp "net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
pilosa "github.com/featurebasedb/featurebase/v3"
|
|
"github.com/featurebasedb/featurebase/v3/boltdb"
|
|
"github.com/featurebasedb/featurebase/v3/encoding/proto"
|
|
"github.com/featurebasedb/featurebase/v3/pql"
|
|
"github.com/featurebasedb/featurebase/v3/server"
|
|
"github.com/featurebasedb/featurebase/v3/test"
|
|
)
|
|
|
|
func TestHandler_PostSchemaCluster(t *testing.T) {
|
|
cluster := test.MustRunUnsharedCluster(t, 3)
|
|
defer cluster.Close()
|
|
cmd := cluster.GetNode(0)
|
|
h := cmd.Handler.(*pilosa.Handler).Handler
|
|
|
|
t.Run("PostSchema", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/schema", strings.NewReader(`{"indexes":[{"name":"blah","options":{"keys":false,"trackExistence":true},"fields":[{"name":"f1","options":{"type":"set","cacheType":"ranked","cacheSize":50000,"keys":false}}],"shardWidth":1048576}]}`)))
|
|
if w.Code != gohttp.StatusNoContent {
|
|
bod, err := io.ReadAll(w.Result().Body)
|
|
if err != nil {
|
|
t.Errorf("reading body: %v", err)
|
|
}
|
|
t.Fatalf("unexpected code: %v, bod: %s", w.Code, bod)
|
|
}
|
|
for i := 0; i < cluster.Len(); i++ {
|
|
cmd = cluster.GetNode(i)
|
|
idx, err := cmd.API.Index(context.Background(), "blah")
|
|
if err != nil {
|
|
t.Fatalf("getting index: %v", err)
|
|
}
|
|
if idx.Name() != "blah" {
|
|
t.Fatalf("index did not get set, got %v", idx.Name())
|
|
}
|
|
|
|
fld, err := cmd.API.Field(context.Background(), "blah", "f1")
|
|
if err != nil {
|
|
t.Fatalf("getting field: %v", err)
|
|
}
|
|
if fld.Name() != "f1" {
|
|
t.Fatalf("unexpected field: %v", fld.Name())
|
|
}
|
|
}
|
|
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("DELETE", "/index/blah", nil))
|
|
})
|
|
}
|
|
|
|
func TestHandler_Endpoints(t *testing.T) {
|
|
// this test is full of hardcoded indexes and things
|
|
cluster := test.MustRunUnsharedCluster(t, 1)
|
|
defer cluster.Close()
|
|
cmd := cluster.GetNode(0)
|
|
h := cmd.Handler.(*pilosa.Handler).Handler
|
|
holder := cmd.Server.Holder()
|
|
hldr := test.Holder{Holder: holder}
|
|
|
|
// Ensure the handler returns "not found" for invalid paths.
|
|
t.Run("Not Found", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/no_such_path", nil))
|
|
if w.Code != gohttp.StatusNotFound {
|
|
t.Fatalf("invalid status: %d", w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("SchemaEmpty", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/schema", nil))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
body := w.Body.String()
|
|
if body != "{\"indexes\":[]}\n" {
|
|
t.Fatalf("unexpected empty schema: '%v'", body)
|
|
}
|
|
|
|
})
|
|
|
|
t.Run("SchemaDetailsEmpty", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/schema/details", nil))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
body := w.Body.String()
|
|
if body != "{\"indexes\":[]}\n" {
|
|
t.Fatalf("unexpected empty schema: '%v'", body)
|
|
}
|
|
|
|
})
|
|
|
|
t.Run("PostSchema", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/schema", strings.NewReader(`{"indexes":[{"name":"blah","options":{"keys":false,"trackExistence":true},"fields":[{"name":"f1","options":{"type":"set","cacheType":"ranked","cacheSize":50000,"keys":false}}],"shardWidth":1048576}]}`)))
|
|
if w.Code != gohttp.StatusNoContent {
|
|
bod, err := io.ReadAll(w.Result().Body)
|
|
if err != nil {
|
|
t.Errorf("reading body: %v", err)
|
|
}
|
|
t.Fatalf("unexpected code: %v, bod: %s", w.Code, bod)
|
|
}
|
|
idx, err := cmd.API.Index(context.Background(), "blah")
|
|
if err != nil {
|
|
t.Fatalf("getting index: %v", err)
|
|
}
|
|
if idx.Name() != "blah" {
|
|
t.Fatalf("index did not get set, got %v", idx.Name())
|
|
}
|
|
|
|
fld, err := cmd.API.Field(context.Background(), "blah", "f1")
|
|
if err != nil {
|
|
t.Fatalf("getting field: %v", err)
|
|
}
|
|
if fld.Name() != "f1" {
|
|
t.Fatalf("unexpected field: %v", fld.Name())
|
|
}
|
|
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("DELETE", "/index/blah", nil))
|
|
})
|
|
|
|
t.Run("Info", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/info", nil))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
var details map[string]interface{}
|
|
body := w.Body.Bytes()
|
|
err := json.Unmarshal(body, &details)
|
|
if err != nil {
|
|
t.Fatalf("error unmarshalling json body [%s]: %v", body, err)
|
|
}
|
|
sw := details["shardWidth"]
|
|
if sw == nil {
|
|
t.Fatalf("no shardWidth in json body [%s]", body)
|
|
}
|
|
var n float64
|
|
var ok bool
|
|
if n, ok = sw.(float64); !ok {
|
|
t.Fatalf("shardWidth not float64 (%T) in json body [%s]", sw, body)
|
|
}
|
|
if uint64(n) != pilosa.ShardWidth {
|
|
t.Fatalf("incorrect shard width: got %d, expected %d", uint64(n), pilosa.ShardWidth)
|
|
}
|
|
count := details["cpuPhysicalCores"]
|
|
if count == nil {
|
|
t.Fatalf("no cpuPhysicalCores in json body [%s]", body)
|
|
}
|
|
if n, ok = count.(float64); !ok {
|
|
t.Fatalf("cpuPhysicalCores not float64 (%T) in json body [%s]", count, body)
|
|
}
|
|
if int(n) == 0 {
|
|
t.Fatal("cpu count should not be 0")
|
|
}
|
|
})
|
|
|
|
i0 := hldr.MustCreateIndexIfNotExists("i0", pilosa.IndexOptions{})
|
|
const shard = 0
|
|
tx0 := holder.Txf().NewWritableQcx()
|
|
defer tx0.Abort()
|
|
if f, err := i0.CreateFieldIfNotExists("f1", pilosa.OptFieldTypeDefault()); err != nil {
|
|
t.Fatal(err)
|
|
} else if _, err := f.SetBit(tx0, 0, 0, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := i0.CreateFieldIfNotExists("f0", pilosa.OptFieldTypeDefault()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := tx0.Finish(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
i1 := hldr.MustCreateIndexIfNotExists("i1", pilosa.IndexOptions{})
|
|
tx1 := holder.Txf().NewWritableQcx()
|
|
defer tx1.Abort()
|
|
if f, err := i1.CreateFieldIfNotExists("f0", pilosa.OptFieldTypeDefault()); err != nil {
|
|
t.Fatal(err)
|
|
} else if _, err := f.SetBit(tx1, 0, 0, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := tx1.Finish(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Run("Schema", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/schema", nil))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
|
|
var bodySchema pilosa.Schema
|
|
if err := json.Unmarshal(w.Body.Bytes(),
|
|
&bodySchema); err != nil {
|
|
t.Fatalf("unexpected unmarshalling error: %v", err)
|
|
}
|
|
// DO NOT COMPARE `CreatedAt` - reset to 0
|
|
for _, i := range bodySchema.Indexes {
|
|
i.CreatedAt = 0
|
|
for _, f := range i.Fields {
|
|
f.CreatedAt = 0
|
|
}
|
|
}
|
|
//
|
|
|
|
var targetSchema pilosa.Schema
|
|
if err := json.Unmarshal([]byte(fmt.Sprintf(`{"indexes":[{"name":"i0","options":{"keys":false,"trackExistence":false},"fields":[{"name":"f0","options":{"type":"set","cacheType":"ranked","cacheSize":50000,"keys":false}},{"name":"f1","options":{"type":"set","cacheType":"ranked","cacheSize":50000,"keys":false}}],"shardWidth":%d},{"name":"i1","options":{"keys":false,"trackExistence":false},"fields":[{"name":"f0","options":{"type":"set","cacheType":"ranked","cacheSize":50000,"keys":false}}],"shardWidth":%[1]d}]}`, pilosa.ShardWidth)),
|
|
&targetSchema); err != nil {
|
|
t.Fatalf("unexpected unmarshalling error: %v", err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(targetSchema, bodySchema) {
|
|
t.Fatalf("target: %+v\nbody: %+v\n", targetSchema, bodySchema)
|
|
}
|
|
})
|
|
|
|
// i2 is for SchemaDetails
|
|
i2 := hldr.MustCreateIndexIfNotExists("i2", pilosa.IndexOptions{})
|
|
tx2 := holder.Txf().NewWritableQcx()
|
|
defer tx2.Abort()
|
|
if f, err := i2.CreateFieldIfNotExists("f0", pilosa.OptFieldTypeSet(pilosa.CacheTypeRanked, 1000)); err != nil {
|
|
t.Fatal(err)
|
|
} else if _, err := f.SetBit(tx2, 0, 0, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f, err := i2.CreateFieldIfNotExists("f1", pilosa.OptFieldTypeInt(-100, 100))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for n := 0; n < 4; n++ {
|
|
if _, err := f.SetValue(tx2, uint64(n), int64(n)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
f, err = i2.CreateFieldIfNotExists("f2", pilosa.OptFieldTypeDecimal(1, pql.NewDecimal(-10, 0), pql.NewDecimal(10, 0)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for n := 0; n < 5; n++ {
|
|
if _, err := f.SetValue(tx2, uint64(n), int64(n)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if f, err := i2.CreateFieldIfNotExists("f3", pilosa.OptFieldTypeTime(pilosa.TimeQuantum("YMDH"), "0")); err != nil {
|
|
t.Fatal(err)
|
|
} else if _, err := f.SetBit(tx2, 0, 0, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f, err := i2.CreateFieldIfNotExists("f4", pilosa.OptFieldTypeMutex(pilosa.CacheTypeRanked, 5000)); err != nil {
|
|
t.Fatal(err)
|
|
} else if _, err := f.SetBit(tx2, 0, 0, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f, err := i2.CreateFieldIfNotExists("f5", pilosa.OptFieldTypeBool()); err != nil {
|
|
t.Fatal(err)
|
|
} else if _, err := f.SetBit(tx2, 0, 0, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := tx2.Finish(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Run("SchemaDetails", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/schema/details", nil))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
|
|
var bodySchema pilosa.Schema
|
|
if err := json.Unmarshal(w.Body.Bytes(), &bodySchema); err != nil {
|
|
t.Fatalf("unexpected unmarshalling error: %v", err)
|
|
}
|
|
// DO NOT COMPARE `CreatedAt` - reset to 0
|
|
for _, i := range bodySchema.Indexes {
|
|
i.CreatedAt = 0
|
|
for _, f := range i.Fields {
|
|
f.CreatedAt = 0
|
|
}
|
|
}
|
|
//
|
|
|
|
var targetSchema pilosa.Schema
|
|
target := fmt.Sprintf(`{"indexes":[{"name":"i0","options":{"keys":false,"trackExistence":false},"fields":[{"name":"f0","options":{"type":"set","cacheType":"ranked","cacheSize":50000,"keys":false}},{"name":"f1","options":{"type":"set","cacheType":"ranked","cacheSize":50000,"keys":false},"views":[{"name":"standard"}]}],"shardWidth":%[1]d},{"name":"i1","options":{"keys":false,"trackExistence":false},"fields":[{"name":"f0","options":{"type":"set","cacheType":"ranked","cacheSize":50000,"keys":false},"views":[{"name":"standard"}]}],"shardWidth":%[1]d},{"name":"i2","options":{"keys":false,"trackExistence":false},"fields":[{"name":"f0","options":{"type":"set","cacheType":"ranked","cacheSize":1000,"keys":false},"views":[{"name":"standard"}]},{"name":"f1","options":{"type":"int","base":0,"bitDepth":0,"min":-100,"max":100,"keys":false,"foreignIndex":""},"views":[{"name":"bsig_f1"}]},{"name":"f2","options":{"type":"decimal","base":0,"scale":1,"bitDepth":0,"min":-10,"max":10,"keys":false},"views":[{"name":"bsig_f2"}]},{"name":"f3","options":{"type":"time","timeQuantum":"YMDH","keys":false,"noStandardView":false},"views":[{"name":"standard"}]},{"name":"f4","options":{"type":"mutex","cacheType":"ranked","cacheSize":5000,"keys":false},"views":[{"name":"standard"}]},{"name":"f5","options":{"type":"bool"},"views":[{"name":"standard"}]}],"shardWidth":%[1]d}]}`, pilosa.ShardWidth)
|
|
if err := json.Unmarshal([]byte(target), &targetSchema); err != nil {
|
|
t.Fatalf("unexpected unmarshalling error: %v", err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(targetSchema, bodySchema) {
|
|
t.Fatalf("target: %+v\nbody: %+v\n", targetSchema, bodySchema)
|
|
}
|
|
})
|
|
|
|
t.Run("Import", func(t *testing.T) {
|
|
indexInfo, err := cmd.API.Schema(context.Background(), false)
|
|
if err != nil {
|
|
t.Fatalf("getting schema: %v", err)
|
|
}
|
|
|
|
idx := indexInfo[0]
|
|
fld := indexInfo[0].Fields[0]
|
|
msg := pilosa.ImportRequest{
|
|
Index: idx.Name,
|
|
IndexCreatedAt: idx.CreatedAt,
|
|
Field: fld.Name,
|
|
FieldCreatedAt: fld.CreatedAt,
|
|
Shard: 0,
|
|
}
|
|
ser := proto.Serializer{}
|
|
data, err := ser.Marshal(&msg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := fmt.Sprintf("/index/%s/field/%s/import", idx.Name, fld.Name)
|
|
httpReq := test.MustNewHTTPRequest("POST", path, bytes.NewBuffer(data))
|
|
httpReq.Header.Set("Content-Type", "application/x-protobuf")
|
|
httpReq.Header.Set("Accept", "application/x-protobuf")
|
|
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, httpReq)
|
|
if w.Code != 200 {
|
|
t.Fatalf(w.Body.String())
|
|
}
|
|
|
|
msg.IndexCreatedAt = -idx.CreatedAt
|
|
msg.FieldCreatedAt = -fld.CreatedAt
|
|
data, err = ser.Marshal(&msg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
httpReq = test.MustNewHTTPRequest("POST", path, bytes.NewBuffer(data))
|
|
httpReq.Header.Set("Content-Type", "application/x-protobuf")
|
|
httpReq.Header.Set("Accept", "application/x-protobuf")
|
|
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, httpReq)
|
|
|
|
if w.Code != 412 {
|
|
t.Fatalf("expected: Precondition Failed, got: %d", w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("ImportRoaring", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
roaringData, _ := hex.DecodeString("3B3001000100000900010000000100010009000100")
|
|
|
|
idx, err := cmd.API.Index(context.Background(), "i0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fld, err := cmd.API.Field(context.Background(), "i0", "f1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
msg := pilosa.ImportRoaringRequest{
|
|
IndexCreatedAt: idx.CreatedAt(),
|
|
FieldCreatedAt: fld.CreatedAt(),
|
|
Clear: false,
|
|
Views: map[string][]byte{
|
|
"": roaringData,
|
|
},
|
|
}
|
|
ser := proto.Serializer{}
|
|
data, err := ser.Marshal(&msg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
httpReq := test.MustNewHTTPRequest("POST", "/index/i0/field/f1/import-roaring/0", bytes.NewBuffer(data))
|
|
httpReq.Header.Set("Content-Type", "application/x-protobuf")
|
|
httpReq.Header.Set("Accept", "application/x-protobuf")
|
|
h.ServeHTTP(w, httpReq)
|
|
if w.Code != 200 {
|
|
t.Fatalf("Unexpected response body: %s", w.Body.String())
|
|
}
|
|
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.PairsField{
|
|
Pairs: []pilosa.Pair{
|
|
{Count: 12, ID: 0},
|
|
},
|
|
Field: "f1",
|
|
}) {
|
|
t.Fatalf("Unexpected result %v", resp.Results[0])
|
|
}
|
|
})
|
|
|
|
t.Run("ImportRoaringOverwrite", func(t *testing.T) {
|
|
if _, err := i0.CreateFieldIfNotExists("int-field", pilosa.OptFieldTypeInt(0, 10)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
|
|
// byShardWidth is a map of the same roaring (fragment) data generated
|
|
// with different shard widths.
|
|
// TODO: a better approach may be to generate this in the test based
|
|
// on shard width.
|
|
byShardWidth := make(map[uint64][]byte)
|
|
// col/val: 3/3, 8/8
|
|
byShardWidth[1<<20] = []byte{60, 48, 0, 1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 32, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 80, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 76, 0, 0, 0, 78, 0, 0, 0, 80, 0, 0, 0, 3, 0, 8, 0, 3, 0, 3, 0, 8, 0}
|
|
byShardWidth[1<<22] = []byte{60, 48, 0, 1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 128, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 64, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 72, 0, 0, 0, 76, 0, 0, 0, 78, 0, 0, 0, 80, 0, 0, 0, 3, 0, 8, 0, 3, 0, 3, 0, 8, 0}
|
|
|
|
var roaringData []byte
|
|
if data, ok := byShardWidth[pilosa.ShardWidth]; ok {
|
|
roaringData = data
|
|
}
|
|
|
|
msg := pilosa.ImportRoaringRequest{
|
|
Action: pilosa.RequestActionOverwrite,
|
|
Block: 0,
|
|
Views: map[string][]byte{
|
|
"bsig_int-field": roaringData,
|
|
},
|
|
}
|
|
ser := proto.Serializer{}
|
|
data, err := ser.Marshal(&msg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
httpReq := test.MustNewHTTPRequest("POST", "/index/i0/field/int-field/import-roaring/0", bytes.NewBuffer(data))
|
|
httpReq.Header.Set("Content-Type", "application/x-protobuf")
|
|
httpReq.Header.Set("Accept", "application/x-protobuf")
|
|
|
|
h.ServeHTTP(w, httpReq)
|
|
resp, err := cmd.API.Query(context.Background(), &pilosa.QueryRequest{Index: "i0", Query: "Row(int-field>0)"})
|
|
if err != nil {
|
|
t.Fatalf("querying: %v", err)
|
|
}
|
|
if row := resp.Results[0].(*pilosa.Row); !reflect.DeepEqual(row.Columns(), []uint64{3, 8}) {
|
|
t.Fatalf("Unexpected result %v", row.Columns())
|
|
}
|
|
})
|
|
|
|
t.Run("Status", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/status", nil))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
ret := mustJSONDecode(t, w.Body)
|
|
if ret["state"].(string) != "NORMAL" {
|
|
t.Fatalf("wrong state from /status: %#v", ret)
|
|
}
|
|
if len(ret["nodes"].([]interface{})) != 1 {
|
|
t.Fatalf("wrong length nodes list: %#v", ret)
|
|
}
|
|
})
|
|
|
|
t.Run("UI/shard-distribution", func(t *testing.T) {
|
|
// This tests the response structure, not the cluster behavior.
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/ui/shard-distribution", nil))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
|
|
ret := mustJSONDecode(t, w.Body)
|
|
|
|
for indexName := range ret {
|
|
indexData := ret[indexName].(map[string]interface{})
|
|
for nodeName := range indexData {
|
|
nodeData := indexData[nodeName].(map[string]interface{})
|
|
_, hasPrimary := nodeData["primary-shards"]
|
|
_, hasReplica := nodeData["replica-shards"]
|
|
|
|
responseOK := hasPrimary && hasReplica
|
|
if !responseOK {
|
|
t.Fatalf("unexpected response structure")
|
|
}
|
|
}
|
|
|
|
}
|
|
})
|
|
|
|
t.Run("Metrics", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/metrics", nil))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("Metrics.json", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/metrics.json", nil))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
mustJSONDecode(t, w.Body)
|
|
})
|
|
|
|
hldr.SetBit("i0", "f0", 30, (1*pilosa.ShardWidth)+1)
|
|
hldr.SetBit("i0", "f0", 30, (1*pilosa.ShardWidth)+2)
|
|
hldr.SetBit("i0", "f0", 30, (3*pilosa.ShardWidth)+4)
|
|
|
|
hldr.SetBit("i0", "f0", 31, 1)
|
|
|
|
hldr.SetBit("i1", "f1", 40, (0*pilosa.ShardWidth)+1)
|
|
hldr.SetBit("i1", "f1", 40, (0*pilosa.ShardWidth)+2)
|
|
hldr.SetBit("i1", "f1", 40, (0*pilosa.ShardWidth)+8)
|
|
|
|
t.Run("Max Shard", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/internal/shards/max", nil))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else if body := w.Body.String(); body != `{"standard":{"i0":3,"i1":0,"i2":0}}`+"\n" {
|
|
t.Fatalf("unexpected body: %s", body)
|
|
}
|
|
})
|
|
|
|
t.Run("Shards args", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/i0/query?shards=0,1", strings.NewReader("Count(Row(f0=30))")))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d %s", w.Code, w.Body.String())
|
|
} else if body := w.Body.String(); body != `{"results":[2]}`+"\n" {
|
|
t.Fatalf("unexpected body: %q", body)
|
|
}
|
|
})
|
|
|
|
t.Run("Shards args protobuf", func(t *testing.T) {
|
|
// Generate request body.
|
|
reqBody, err := cmd.API.Serializer.Marshal(&pilosa.QueryRequest{
|
|
Query: "Count(Row(f0=30))",
|
|
Shards: []uint64{0, 1},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Generate protobuf request.
|
|
req := test.MustNewHTTPRequest("POST", "/index/i0/query", bytes.NewReader(reqBody))
|
|
req.Header.Set("Content-Type", "application/x-protobuf")
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else if body := w.Body.String(); body != `{"results":[2]}`+"\n" {
|
|
t.Fatalf("unexpected body: %q", body)
|
|
} else if w.Header().Get("Content-Type") != "application/json" {
|
|
t.Fatalf("unexpected header: %q", w.Header().Get("Content-Type"))
|
|
}
|
|
|
|
})
|
|
|
|
t.Run("Query args error", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/i0/query?shards=a,b", strings.NewReader("Count(Row(f0=30))")))
|
|
if w.Code != gohttp.StatusBadRequest {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else if body := w.Body.String(); body != `{"error":"invalid shard argument"}`+"\n" {
|
|
t.Fatalf("unexpected body: %q", body)
|
|
}
|
|
})
|
|
|
|
t.Run("Query params err", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/i0/query?shards=0,1&db=sample", strings.NewReader("Count(Row(f0=30))")))
|
|
if w.Code != gohttp.StatusBadRequest {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else if body := w.Body.String(); body != `{"error":"db is not a valid argument"}`+"\n" {
|
|
t.Fatalf("unexpected body: %q", body)
|
|
}
|
|
})
|
|
|
|
t.Run("Uint64 protobuf", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
r := test.MustNewHTTPRequest("POST", "/index/i0/query", strings.NewReader("Count(Row(f0=30))"))
|
|
r.Header.Set("Accept", "application/x-protobuf")
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
|
|
var resp pilosa.QueryResponse
|
|
if err := cmd.API.Serializer.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
} else if rt, ok := resp.Results[0].(uint64); !ok || rt != 3 {
|
|
t.Fatalf("unexpected response type: %#v", resp.Results[0])
|
|
} else if w.Header().Get("Content-Type") != "application/protobuf" {
|
|
t.Fatalf("unexpected header: %q", w.Header().Get("Content-Type"))
|
|
}
|
|
})
|
|
|
|
t.Run("Row JSON", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/i0/query", strings.NewReader("Row(f0=30)")))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else if body := w.Body.String(); body != fmt.Sprintf(`{"results":[{"columns":[%d,%d,%d]}]}`, pilosa.ShardWidth+1, pilosa.ShardWidth+2, 3*pilosa.ShardWidth+4)+"\n" {
|
|
t.Fatalf("unexpected body: %s", body)
|
|
}
|
|
})
|
|
|
|
t.Run("Row pbuf", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
r := test.MustNewHTTPRequest("POST", "/index/i0/query", strings.NewReader("Row(f0=30)"))
|
|
r.Header.Set("Accept", "application/x-protobuf")
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
|
|
var resp pilosa.QueryResponse
|
|
if err := cmd.API.Serializer.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
} else if columns := resp.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(columns, []uint64{pilosa.ShardWidth + 1, pilosa.ShardWidth + 2, (3 * pilosa.ShardWidth) + 4}) {
|
|
t.Fatalf("unexpected columns: %+v", columns)
|
|
}
|
|
})
|
|
|
|
t.Run("Query Pairs JSON", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/i0/query", strings.NewReader(`TopN(f0, n=2)`)))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else if body := w.Body.String(); body != `{"results":[[{"id":30,"key":"","count":3},{"id":31,"key":"","count":1}]]}`+"\n" {
|
|
t.Fatalf("unexpected body: %q", body)
|
|
}
|
|
})
|
|
|
|
t.Run("Query Pairs protobuf", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
r := test.MustNewHTTPRequest("POST", "/index/i0/query", strings.NewReader(`TopN(f0, n=2)`))
|
|
r.Header.Set("Accept", "application/x-protobuf")
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
|
|
var resp pilosa.QueryResponse
|
|
if err := cmd.API.Serializer.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
} else if a := resp.Results[0].(*pilosa.PairsField); len(a.Pairs) != 2 {
|
|
t.Fatalf("unexpected pair length: %d", len(a.Pairs))
|
|
}
|
|
})
|
|
|
|
t.Run("Query err JSON", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/i0/query", strings.NewReader(`Row(row=30)`)))
|
|
if w.Code != gohttp.StatusBadRequest {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else if body := w.Body.String(); body != `{"error":"executing: translating call: validating value for field \"row\": field not found"}`+"\n" {
|
|
t.Fatalf("unexpected body: %q", body)
|
|
}
|
|
})
|
|
|
|
t.Run("Query err protobuf", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
r := test.MustNewHTTPRequest("POST", "/index/i0/query", strings.NewReader(`Row(row=30)`))
|
|
r.Header.Set("Accept", "application/x-protobuf")
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusBadRequest {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
|
|
var resp pilosa.QueryResponse
|
|
if err := cmd.API.Serializer.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
} else if s := resp.Err.Error(); s != `executing: translating call: validating value for field "row": field not found` {
|
|
t.Fatalf("unexpected error: %s", s)
|
|
}
|
|
})
|
|
|
|
t.Run("Query empty", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/i0/query", strings.NewReader("")))
|
|
if body := w.Body.String(); body != `{"results":[]}`+"\n" && body != `{"results":null}`+"\n" {
|
|
t.Fatalf("unexpected body: %q", body)
|
|
}
|
|
})
|
|
|
|
t.Run("Query int field unbounded", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
fieldName := "f-int-ubound"
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", fmt.Sprintf("/index/i0/field/%s", fieldName),
|
|
strings.NewReader(`{"options":{"type":"int"}}`)))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/schema", strings.NewReader("")))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
rsp := getSchemaResponse{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &rsp); err != nil {
|
|
t.Fatalf("json decode: %s", err)
|
|
}
|
|
field := rsp.findField("i0", fieldName)
|
|
if field == nil {
|
|
t.Fatalf("field not found: %s", fieldName)
|
|
}
|
|
if field != nil { // happy linter
|
|
if !reflect.DeepEqual(pql.NewDecimal(math.MinInt64, 0), field.Options.Min) {
|
|
t.Fatalf("field min %v != %v", int64(math.MinInt64), field.Options.Min)
|
|
}
|
|
if !reflect.DeepEqual(pql.NewDecimal(math.MaxInt64, 0), field.Options.Max) {
|
|
t.Fatalf("field max %v != %v", int64(math.MaxInt64), field.Options.Max)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Query int field unbounded min", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
fieldName := "f-int-ubound-min"
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", fmt.Sprintf("/index/i0/field/%s", fieldName),
|
|
strings.NewReader(`{"options":{"type":"int", "max": 10}}`)))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/schema", strings.NewReader("")))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
rsp := getSchemaResponse{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &rsp); err != nil {
|
|
t.Fatalf("json decode: %s", err)
|
|
}
|
|
field := rsp.findField("i0", fieldName)
|
|
if field == nil {
|
|
t.Fatalf("field not found: %s", fieldName)
|
|
}
|
|
if field != nil { // happy linter
|
|
if !reflect.DeepEqual(pql.NewDecimal(math.MinInt64, 0), field.Options.Min) {
|
|
t.Fatalf("field min %v != %v", int64(math.MinInt64), field.Options.Min)
|
|
}
|
|
if !reflect.DeepEqual(pql.NewDecimal(1, -1), field.Options.Max) {
|
|
t.Fatalf("field max %v != %v", 10, field.Options.Max)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Query int field unbounded max", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
fieldName := "f-int-ubound-max"
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", fmt.Sprintf("/index/i0/field/%s", fieldName),
|
|
strings.NewReader(`{"options":{"type":"int", "min": -10}}`)))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/schema", strings.NewReader("")))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
rsp := getSchemaResponse{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &rsp); err != nil {
|
|
t.Fatalf("json decode: %s", err)
|
|
}
|
|
field := rsp.findField("i0", fieldName)
|
|
if field == nil {
|
|
t.Fatalf("field not found: %s", fieldName)
|
|
}
|
|
if field != nil { // happy linter
|
|
if !reflect.DeepEqual(pql.NewDecimal(-1, -1), field.Options.Min) {
|
|
t.Fatalf("field min %v != %v", 10, field.Options.Min)
|
|
}
|
|
if !reflect.DeepEqual(pql.NewDecimal(math.MaxInt64, 0), field.Options.Max) {
|
|
t.Fatalf("field max %v != %v", int64(math.MaxInt64), field.Options.Max)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Query int field min > max return 400", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
fieldName := "f-int-ubound-err"
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", fmt.Sprintf("/index/i0/field/%s", fieldName),
|
|
strings.NewReader(`{"options":{"type":"int", "min": 10, "max": -10}}`)))
|
|
if w.Code != gohttp.StatusBadRequest {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("Query decimal field unbounded", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
fieldName := "f-decimal-ubound"
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", fmt.Sprintf("/index/i0/field/%s", fieldName),
|
|
strings.NewReader(`{"options":{"type":"decimal", "scale": 0}}`)))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/schema", strings.NewReader("")))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
rsp := getSchemaResponse{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &rsp); err != nil {
|
|
t.Fatalf("json decode: %s", err)
|
|
}
|
|
field := rsp.findField("i0", fieldName)
|
|
if field == nil {
|
|
t.Fatalf("field not found: %s", fieldName)
|
|
}
|
|
if field != nil { // happy linter
|
|
if !reflect.DeepEqual(pql.NewDecimal(math.MinInt64, 0), field.Options.Min) {
|
|
t.Fatalf("field min %v != %v", int64(math.MinInt64), field.Options.Min)
|
|
}
|
|
if !reflect.DeepEqual(pql.NewDecimal(math.MaxInt64, 0), field.Options.Max) {
|
|
t.Fatalf("field max %v != %v", int64(math.MaxInt64), field.Options.Max)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Query decimal field unbounded min", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
fieldName := "f-decimal-ubound-min"
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", fmt.Sprintf("/index/i0/field/%s", fieldName),
|
|
strings.NewReader(`{"options":{"type":"decimal", "scale": 1, "max": 10.5}}`)))
|
|
if w.Code != gohttp.StatusOK {
|
|
fmt.Println(w.Body.String())
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/schema", strings.NewReader("")))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
rsp := getSchemaResponse{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &rsp); err != nil {
|
|
t.Fatalf("json decode: %s", err)
|
|
}
|
|
field := rsp.findField("i0", fieldName)
|
|
if field == nil {
|
|
t.Fatalf("field not found: %s", fieldName)
|
|
}
|
|
if field != nil { // happy linter
|
|
if !reflect.DeepEqual(pql.NewDecimal(math.MinInt64, 1), field.Options.Min) {
|
|
t.Fatalf("field min %v != %v", pql.NewDecimal(math.MinInt64, 1), field.Options.Min)
|
|
}
|
|
if !reflect.DeepEqual(pql.NewDecimal(105, 1), field.Options.Max) {
|
|
t.Fatalf("field max %v != %v", pql.NewDecimal(105, 1), field.Options.Max)
|
|
}
|
|
}
|
|
})
|
|
t.Run("Query decimal field scale only", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
fieldName := "f-decimal-scale-only"
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", fmt.Sprintf("/index/i0/field/%s", fieldName),
|
|
strings.NewReader(`{"options":{"type":"decimal", "scale": 2}}`)))
|
|
if w.Code != gohttp.StatusOK {
|
|
fmt.Println(w.Body.String())
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/schema", strings.NewReader("")))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
rsp := getSchemaResponse{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &rsp); err != nil {
|
|
t.Fatalf("json decode: %s", err)
|
|
}
|
|
field := rsp.findField("i0", fieldName)
|
|
if field == nil {
|
|
t.Fatalf("field not found: %s", fieldName)
|
|
}
|
|
if field != nil { // happy linter
|
|
if !reflect.DeepEqual(pql.NewDecimal(math.MinInt64, 2), field.Options.Min) {
|
|
t.Fatalf("field min %v != %v", pql.NewDecimal(math.MinInt64, 1), field.Options.Min)
|
|
}
|
|
if !reflect.DeepEqual(pql.NewDecimal(math.MaxInt64, 2), field.Options.Max) {
|
|
t.Fatalf("field min %v != %v", pql.NewDecimal(math.MaxInt64, 2), field.Options.Max)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Ensure that decimal fields error when scale is not provided.
|
|
t.Run("Query decimal field scale error", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
fieldName := "f-decimal-ubound"
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", fmt.Sprintf("/index/i0/field/%s", fieldName),
|
|
strings.NewReader(`{"options":{"type":"decimal"}}`)))
|
|
expErr := "decimal field requires a scale argument"
|
|
if w.Code != gohttp.StatusBadRequest {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else if !strings.Contains(w.Body.String(), expErr) {
|
|
t.Fatalf("expected error to contain: %s, but got: %s", expErr, w.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("Method not allowed", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/index/i0/query", nil))
|
|
if w.Code != gohttp.StatusMethodNotAllowed {
|
|
t.Fatalf("invalid status: %d", w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("Err Parse", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/idx0/query?shards=0,1", strings.NewReader("bad_fn(")))
|
|
if w.Code != gohttp.StatusBadRequest {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else if body := w.Body.String(); body != `{"error":"parsing: parsing: \nparse error near IDENT (line 1 symbol 1 - line 1 symbol 4):\n\"bad\"\n"}`+"\n" {
|
|
t.Fatalf("unexpected body: %s", body)
|
|
}
|
|
})
|
|
|
|
t.Run("delete index", func(t *testing.T) {
|
|
hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{})
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("DELETE", "/index/i", strings.NewReader("")))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d, body: %s", w.Code, w.Body.String())
|
|
} else {
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
if !resp.Success {
|
|
t.Fatalf("unexpected body: %q", w.Body.String())
|
|
}
|
|
}
|
|
// Verify index is gone.
|
|
if hldr.Index("i") != nil {
|
|
t.Fatal("expected nil index")
|
|
}
|
|
})
|
|
|
|
t.Run("Field delete", func(t *testing.T) {
|
|
i := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{})
|
|
if _, err := i.CreateFieldIfNotExists("f1", pilosa.OptFieldTypeDefault()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("DELETE", "/index/i/field/f1", strings.NewReader("")))
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d, body: %s", w.Code, w.Body.String())
|
|
} else {
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
if !resp.Success {
|
|
t.Fatalf("unexpected body: %q", w.Body.String())
|
|
}
|
|
|
|
if f := hldr.Index("i").Field("f1"); f != nil {
|
|
t.Fatal("expected nil field")
|
|
}
|
|
}
|
|
})
|
|
|
|
hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{})
|
|
|
|
t.Run("Version", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
r := test.MustNewHTTPRequest("GET", "/version", nil)
|
|
h.ServeHTTP(w, r)
|
|
version := strings.TrimPrefix(pilosa.Version, "v")
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else if w.Body.String() != `{"version":"`+version+`"}`+"\n" {
|
|
t.Fatalf("unexpected body: %q", w.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("Fragment Nodes", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
r := test.MustNewHTTPRequest("GET", "/internal/fragment/nodes?index=i&shard=0", nil)
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
body := mustJSONDecodeSlice(t, w.Body)
|
|
bmap := body[0].(map[string]interface{})
|
|
if bmap["isPrimary"] != true {
|
|
t.Fatalf("expected true primary, got: %+v", bmap)
|
|
}
|
|
|
|
// invalid argument should return BadRequest
|
|
w = httptest.NewRecorder()
|
|
r = test.MustNewHTTPRequest("GET", "/internal/fragment/nodes?db=X&shard=0", nil)
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusBadRequest {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
|
|
// index is required
|
|
w = httptest.NewRecorder()
|
|
r = test.MustNewHTTPRequest("GET", "/internal/fragment/nodes?shard=0", nil)
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusBadRequest {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("Expvars", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
r := test.MustNewHTTPRequest("GET", "/debug/vars", nil)
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("Recalculate Caches", func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/recalculate-caches", nil))
|
|
if w.Code != gohttp.StatusNoContent {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("CORS", func(t *testing.T) {
|
|
req := test.MustNewHTTPRequest("OPTIONS", "/index/foo/query", nil)
|
|
req.Header.Add("Origin", "http://test/")
|
|
req.Header.Add("Access-Control-Request-Method", "POST")
|
|
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
result := w.Result()
|
|
|
|
// This handler does not support CORS, return Method Not Allowed (405)
|
|
if result.StatusCode != 405 {
|
|
t.Fatalf("CORS preflight status should be 405, but is %v", result.StatusCode)
|
|
}
|
|
|
|
clus := test.MustRunCluster(t, 1, []server.CommandOption{test.OptAllowedOrigins([]string{"http://test/"})})
|
|
defer clus.Close()
|
|
w = httptest.NewRecorder()
|
|
h1 := clus.GetNode(0).Handler.(*pilosa.Handler).Handler
|
|
h1.ServeHTTP(w, req)
|
|
result = w.Result()
|
|
|
|
if result.StatusCode != 200 {
|
|
t.Fatalf("CORS preflight status should be 200, but is %v", result.StatusCode)
|
|
}
|
|
|
|
if result.Header["Access-Control-Allow-Origin"][0] != "http://test/" {
|
|
t.Fatal("CORS header not present")
|
|
}
|
|
})
|
|
|
|
t.Run("index handlers", func(t *testing.T) {
|
|
|
|
// create index
|
|
w := httptest.NewRecorder()
|
|
r := test.MustNewHTTPRequest("POST", "/index/idx1", strings.NewReader(""))
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else {
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
if !resp.Success {
|
|
t.Fatalf("unexpected body: %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// create index again
|
|
w = httptest.NewRecorder()
|
|
r = test.MustNewHTTPRequest("POST", "/index/idx1", strings.NewReader(""))
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusConflict {
|
|
t.Errorf("unexpected status code: %d", w.Code)
|
|
} else {
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
Name string `json:"name,omitempty"`
|
|
CreatedAt int64 `json:"createdAt,omitempty"`
|
|
}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
if resp.Success || resp.Name == "" || resp.CreatedAt == 0 {
|
|
t.Errorf("unexpected body: %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// create field
|
|
w = httptest.NewRecorder()
|
|
r = test.MustNewHTTPRequest("POST", "/index/idx1/field/fld1", strings.NewReader(""))
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else {
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
Name string `json:"name,omitempty"`
|
|
CreatedAt int64 `json:"createdAt,omitempty"`
|
|
}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
if !resp.Success || resp.Name == "" || resp.CreatedAt == 0 {
|
|
t.Fatalf("unexpected body: %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// create field again
|
|
w = httptest.NewRecorder()
|
|
r = test.MustNewHTTPRequest("POST", "/index/idx1/field/fld1", strings.NewReader(""))
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusConflict {
|
|
t.Errorf("unexpected status code: %d", w.Code)
|
|
} else {
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
Name string `json:"name,omitempty"`
|
|
CreatedAt int64 `json:"createdAt,omitempty"`
|
|
}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
if resp.Success || resp.Name == "" || resp.CreatedAt == 0 {
|
|
t.Errorf("unexpected body: %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// delete field
|
|
w = httptest.NewRecorder()
|
|
r = test.MustNewHTTPRequest("DELETE", "/index/idx1/field/fld1", strings.NewReader(""))
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else {
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
if !resp.Success {
|
|
t.Fatalf("unexpected body: %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// delete field again
|
|
w = httptest.NewRecorder()
|
|
r = test.MustNewHTTPRequest("DELETE", "/index/idx1/field/fld1", strings.NewReader(""))
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusNotFound {
|
|
t.Errorf("unexpected status code: %d", w.Code)
|
|
} else if w.Body.String() != `{"success":false,"error":{"message":"deleting field: fld1: field not found"}}`+"\n" {
|
|
t.Errorf("unexpected body: %q", w.Body.String())
|
|
}
|
|
|
|
// delete index
|
|
w = httptest.NewRecorder()
|
|
r = test.MustNewHTTPRequest("DELETE", "/index/idx1", strings.NewReader(""))
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else {
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
if !resp.Success {
|
|
t.Fatalf("unexpected body: %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// delete index again
|
|
w = httptest.NewRecorder()
|
|
r = test.MustNewHTTPRequest("DELETE", "/index/idx1", strings.NewReader(""))
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusNotFound {
|
|
t.Errorf("unexpected status code: %d", w.Code)
|
|
} else {
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
if resp.Success {
|
|
t.Fatalf("unexpected body: %q", w.Body.String())
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("translate keys", func(t *testing.T) {
|
|
// create index
|
|
w := httptest.NewRecorder()
|
|
r := test.MustNewHTTPRequest("POST", "/index/i1-tr", strings.NewReader(`{"options":{"keys":true}}`))
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else {
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
if !resp.Success {
|
|
t.Fatalf("unexpected body: %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// create field
|
|
w = httptest.NewRecorder()
|
|
r = test.MustNewHTTPRequest("POST", "/index/i1-tr/field/f1", strings.NewReader(`{"options":{"keys":true}}`))
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
} else {
|
|
var resp struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
if !resp.Success {
|
|
t.Fatalf("unexpected body: %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// set some bits
|
|
w = httptest.NewRecorder()
|
|
r = test.MustNewHTTPRequest("POST", "/index/i1-tr/query", strings.NewReader(`Set("col1", f1="row1")`))
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
|
|
// Generate request body for translate column keys request
|
|
reqBody, err := cmd.API.Serializer.Marshal(&pilosa.TranslateKeysRequest{
|
|
Index: "i1-tr",
|
|
Keys: []string{"col1", "col2", "col3"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Generate protobuf request.
|
|
w = httptest.NewRecorder()
|
|
r = test.MustNewHTTPRequest("POST", "/internal/translate/keys", bytes.NewReader(reqBody))
|
|
r.Header.Set("Content-Type", "application/x-protobuf")
|
|
r.Header.Set("Accept", "application/x-protobuf")
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
var target []uint64
|
|
if pilosa.ShardWidth == 1<<22 {
|
|
target = []uint64{650117121, 637534209, 641728513}
|
|
} else {
|
|
target = []uint64{162529281, 159383553, 160432129}
|
|
}
|
|
resp := pilosa.TranslateKeysResponse{}
|
|
err = cmd.API.Serializer.Unmarshal(w.Body.Bytes(), &resp)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(target, resp.IDs) {
|
|
t.Fatalf("%v != %v", target, resp.IDs)
|
|
}
|
|
|
|
// Generate request body for translate row keys request
|
|
reqBody, err = cmd.API.Serializer.Marshal(&pilosa.TranslateKeysRequest{
|
|
Index: "i1-tr",
|
|
Field: "f1",
|
|
Keys: []string{"row1", "row2"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Generate protobuf request.
|
|
w = httptest.NewRecorder()
|
|
r = test.MustNewHTTPRequest("POST", "/internal/translate/keys", bytes.NewReader(reqBody))
|
|
r.Header.Set("Content-Type", "application/x-protobuf")
|
|
r.Header.Set("Accept", "application/x-protobuf")
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != gohttp.StatusOK {
|
|
t.Fatalf("unexpected status code: %d", w.Code)
|
|
}
|
|
target = []uint64{1, 2}
|
|
resp = pilosa.TranslateKeysResponse{}
|
|
err = cmd.API.Serializer.Unmarshal(w.Body.Bytes(), &resp)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(target, resp.IDs) {
|
|
t.Fatalf("%v != %v", target, resp.IDs)
|
|
}
|
|
})
|
|
|
|
t.Run("grpc-web-cors", func(t *testing.T) {
|
|
req := test.MustNewHTTPRequest("OPTIONS", "/pilosa.Pilosa/QueryPQL", nil)
|
|
req.Header.Add("Origin", "http://test/")
|
|
req.Header.Add("Access-Control-Request-Method", "POST")
|
|
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
result := w.Result()
|
|
|
|
// Fail CORS preflight
|
|
if result.Header["Access-Control-Allow-Origin"] != nil {
|
|
t.Fatalf("CORS preflight includes Access-Control-Allow-Origin but should not.")
|
|
}
|
|
|
|
clus := test.MustRunCluster(t, 1, []server.CommandOption{test.OptAllowedOrigins([]string{"http://test/"})})
|
|
defer clus.Close()
|
|
w = httptest.NewRecorder()
|
|
h := clus.GetNode(0).Handler.(*pilosa.Handler).Handler
|
|
h.ServeHTTP(w, req)
|
|
result = w.Result()
|
|
|
|
if result.Header["Access-Control-Allow-Origin"] == nil {
|
|
t.Fatalf("CORS preflight does not include Access-Control-Allow-Origin.")
|
|
}
|
|
|
|
if result.Header["Access-Control-Allow-Origin"][0] != "http://test/" {
|
|
t.Fatal("CORS header not present")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCluster_TranslateStore(t *testing.T) {
|
|
cluster := test.MustNewCluster(t, 1)
|
|
cluster.Nodes[0] = test.NewCommandNode(t,
|
|
server.OptCommandServerOptions(
|
|
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
|
|
pilosa.OptServerOpenTranslateReader(pilosa.GetOpenTranslateReaderWithLockerFunc(nil, &sync.Mutex{})),
|
|
),
|
|
)
|
|
|
|
if err := cluster.Start(); err != nil {
|
|
t.Fatalf("starting node 0: %v", err)
|
|
}
|
|
defer cluster.GetIdleNode(0).Close() // nolint: errcheck
|
|
|
|
test.Do(t, "POST", cluster.GetIdleNode(0).URL()+"/index/i0", "{\"options\": {\"keys\": true}}")
|
|
}
|
|
|
|
func TestClusterTranslator(t *testing.T) {
|
|
cluster := test.MustRunCluster(t, 3,
|
|
[]server.CommandOption{
|
|
server.OptCommandServerOptions(
|
|
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
|
|
)},
|
|
[]server.CommandOption{
|
|
server.OptCommandServerOptions(
|
|
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
|
|
pilosa.OptServerOpenTranslateReader(pilosa.GetOpenTranslateReaderWithLockerFunc(nil, &sync.Mutex{})),
|
|
)},
|
|
[]server.CommandOption{
|
|
server.OptCommandServerOptions(
|
|
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
|
|
)},
|
|
)
|
|
defer cluster.Close()
|
|
|
|
test.Do(t, "POST", cluster.GetNode(0).URL()+"/index/i0", "{\"options\": {\"keys\": true}}")
|
|
test.Do(t, "POST", cluster.GetNode(0).URL()+"/index/i0/field/f0", "{\"options\": {\"keys\": true}}")
|
|
|
|
test.Do(t, "POST", cluster.GetNode(0).URL()+"/index/i0/query", "Set(\"foo\", f0=\"bar\")")
|
|
|
|
var result0, result1 string
|
|
if err := test.RetryUntil(2*time.Second, func() error {
|
|
result0 = test.Do(t, "POST", cluster.GetNode(0).URL()+"/index/i0/query", "Row(f0=\"bar\")").Body
|
|
result1 = test.Do(t, "POST", cluster.GetNode(1).URL()+"/index/i0/query", "Row(f0=\"bar\")").Body
|
|
if result0 != result1 {
|
|
return fmt.Errorf("`%s` != `%s`", result0, result1)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, i := range []string{result0, result1} {
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal([]byte(i), &resp)
|
|
if err != nil {
|
|
t.Fatalf("json unmarshal error: %s", err)
|
|
}
|
|
if results, ok := resp["results"].([]interface{}); ok {
|
|
if result, ok := results[0].(map[string]interface{}); ok {
|
|
if keys, ok := result["keys"].([]interface{}); ok {
|
|
if key, ok := keys[0].(string); ok {
|
|
if key != "foo" {
|
|
t.Fatalf("Key is %s but should be 'foo'", key)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func mustJSONDecode(t *testing.T, r io.Reader) (ret map[string]interface{}) {
|
|
dec := json.NewDecoder(r)
|
|
err := dec.Decode(&ret)
|
|
if err != nil {
|
|
t.Fatalf("decoding response: %v", err)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func mustJSONDecodeSlice(t *testing.T, r io.Reader) (ret []interface{}) {
|
|
dec := json.NewDecoder(r)
|
|
err := dec.Decode(&ret)
|
|
if err != nil {
|
|
t.Fatalf("decoding response: %v", err)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
type getSchemaResponse struct {
|
|
Indexes []*pilosa.IndexInfo `json:"indexes"`
|
|
}
|
|
|
|
func (r getSchemaResponse) findField(indexName, fieldName string) *pilosa.FieldInfo {
|
|
for _, index := range r.Indexes {
|
|
if index.Name == indexName {
|
|
for _, field := range index.Fields {
|
|
if field.Name == fieldName {
|
|
return field
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|