diff --git a/attr.go b/attr.go
index 2e45aadf8..6f12ffdc0 100644
--- a/attr.go
+++ b/attr.go
@@ -41,7 +41,7 @@ const (
// AttrStore represents a storage layer for attributes.
type AttrStore struct {
- mu sync.Mutex
+ mu sync.RWMutex
path string
db *bolt.DB
@@ -92,8 +92,8 @@ func (s *AttrStore) Close() error {
// Attrs returns a set of attributes by ID.
func (s *AttrStore) Attrs(id uint64) (m map[string]interface{}, err error) {
- s.mu.Lock()
- defer s.mu.Unlock()
+ s.mu.RLock()
+ defer s.mu.RUnlock()
// Check cache for map.
if m = s.attrs[id]; m != nil {
@@ -119,6 +119,19 @@ func (s *AttrStore) Attrs(id uint64) (m map[string]interface{}, err error) {
// SetAttrs sets attribute values for a given ID.
func (s *AttrStore) SetAttrs(id uint64, m map[string]interface{}) error {
+ // Ignore empty maps.
+ if len(m) == 0 {
+ return nil
+ }
+
+ // Check if the attributes already exist under a read-only lock.
+ if attr, err := s.Attrs(id); err != nil {
+ return err
+ } else if attr != nil && mapContains(attr, m) {
+ return nil
+ }
+
+ // Obtain write lock.
s.mu.Lock()
defer s.mu.Unlock()
@@ -493,3 +506,14 @@ func (cur *blockCursor) next() (key, value []byte) {
return key, value
}
+
+// mapContains returns true if all keys & values of subset are in m.
+func mapContains(m, subset map[string]interface{}) bool {
+ for k, v := range subset {
+ value, ok := m[k]
+ if !ok || value != v {
+ return false
+ }
+ }
+ return true
+}
diff --git a/attr_test.go b/attr_test.go
index dda280214..d9884cedc 100644
--- a/attr_test.go
+++ b/attr_test.go
@@ -18,6 +18,8 @@ import (
"io/ioutil"
"os"
"reflect"
+ "runtime"
+ "sync"
"testing"
"github.com/pilosa/pilosa"
@@ -143,6 +145,38 @@ func NewAttrStore() *AttrStore {
return &AttrStore{AttrStore: pilosa.NewAttrStore(f.Name())}
}
+func BenchmarkAttrStore_Duplicate(b *testing.B) {
+ s := MustOpenAttrStore()
+ defer s.Close()
+
+ // Set attributes.
+ const n = 5
+ for i := 0; i < n; i++ {
+ if err := s.SetAttrs(uint64(i), map[string]interface{}{"A": 100, "B": "foo", "C": true, "D": 100.2}); err != nil {
+ b.Fatal(err)
+ }
+ }
+
+ b.ReportAllocs()
+ b.ResetTimer()
+
+ // Update attributes with an existing subset.
+ cpuN := runtime.GOMAXPROCS(0)
+ var wg sync.WaitGroup
+ for i := 0; i < cpuN; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for j := 0; j < b.N/cpuN; j++ {
+ if err := s.SetAttrs(uint64(j%n), map[string]interface{}{"A": int64(100), "B": "foo", "D": 100.2}); err != nil {
+ b.Fatal(err)
+ }
+ }
+ }()
+ }
+ wg.Wait()
+}
+
// MustOpenAttrStore returns a new, opened attribute store at a temporary path. Panic on error.
func MustOpenAttrStore() *AttrStore {
s := NewAttrStore()
diff --git a/cmd/root.go b/cmd/root.go
index d50dd05b4..a34e8b0c7 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -109,6 +109,11 @@ func setAllConfig(v *viper.Viper, flags *pflag.FlagSet, envPrefix string) error
v.AutomaticEnv()
c := v.GetString("config")
+ var flagErr error
+ validTags := make(map[string]bool)
+ flags.VisitAll(func(f *pflag.Flag) {
+ validTags[f.Name] = true
+ })
// add config file to viper
if c != "" {
@@ -118,10 +123,16 @@ func setAllConfig(v *viper.Viper, flags *pflag.FlagSet, envPrefix string) error
if err != nil {
return fmt.Errorf("error reading configuration file '%s': %v", c, err)
}
+
+ for _, key := range v.AllKeys() {
+ if _, ok := validTags[key]; !ok {
+ return fmt.Errorf("invalid option in configuration file: %v", key)
+ }
+ }
+
}
// set all values from viper
- var flagErr error
flags.VisitAll(func(f *pflag.Flag) {
if flagErr != nil {
return
diff --git a/cmd/root_test.go b/cmd/root_test.go
index 16bc82a8a..e1aaaf31d 100644
--- a/cmd/root_test.go
+++ b/cmd/root_test.go
@@ -171,3 +171,27 @@ func TestRootCommand(t *testing.T) {
t.Fatalf("Expected standard usage message from RootCommand, but err: '%v', output: '%s'", err, outStr)
}
}
+
+func TestRootCommand_Config(t *testing.T) {
+ file, err := ioutil.TempFile("", "test.conf")
+ if err != nil {
+ panic(err)
+ }
+ config := `data-dir = "/tmp/pil5_0"
+bind = "127.0.0.1:10101"
+
+[cluster]
+ poll-interval = "2m0s"
+ replicas = 2
+ partitions = 128
+ hosts = [
+ "127.0.0.1:10101",
+ "127.0.0.1:10111",
+ ]`
+ file.Write([]byte(config))
+ file.Close()
+ _, err = ExecNewRootCommand(t, "server", "--config", file.Name())
+ if err.Error() != "invalid option in configuration file: cluster.partitions" {
+ t.Fatalf("Expected invalid option in configuration file, but err: '%v'", err)
+ }
+}
diff --git a/ctl/export.go b/ctl/export.go
index fa6277f16..295e10039 100644
--- a/ctl/export.go
+++ b/ctl/export.go
@@ -31,7 +31,7 @@ type ExportCommand struct {
// Name of the index & frame to export from.
Index string
Frame string
- View string
+ View string
// Filename to export to.
Path string
diff --git a/fragment.go b/fragment.go
index 8c0964da1..bf29cae03 100644
--- a/fragment.go
+++ b/fragment.go
@@ -86,6 +86,9 @@ type Fragment struct {
cache Cache
CacheSize uint32
+ // Stats reporting.
+ maxRowID uint64
+
// Cache containing full rows (not just counts).
rowCache BitmapCache
@@ -166,6 +169,11 @@ func (f *Fragment) Open() error {
// Clear checksums.
f.checksums = make(map[int][]byte)
+ // Read last bit to determine max row.
+ pos := f.storage.Max()
+ f.maxRowID = pos / SliceWidth
+ f.stats.Gauge("rows", float64(f.maxRowID))
+
return nil
}(); err != nil {
f.close()
@@ -409,6 +417,12 @@ func (f *Fragment) setBit(rowID, columnID uint64) (changed bool, err error) {
f.stats.Count("setN", 1)
+ // Update row count if they have increased.
+ if rowID > f.maxRowID {
+ f.maxRowID = rowID
+ f.stats.Gauge("rows", float64(f.maxRowID))
+ }
+
return changed, nil
}
diff --git a/handler.go b/handler.go
index c4a98d04c..c4a2c2a06 100644
--- a/handler.go
+++ b/handler.go
@@ -42,6 +42,7 @@ import (
_ "github.com/pilosa/pilosa/statik"
"github.com/rakyll/statik/fs"
+ "unicode"
)
// Handler represents an HTTP handler.
@@ -843,6 +844,12 @@ func (h *Handler) readProtobufQueryRequest(r *http.Request) (*QueryRequest, erro
// readURLQueryRequest parses query parameters from URL parameters from r.
func (h *Handler) readURLQueryRequest(r *http.Request) (*QueryRequest, error) {
q := r.URL.Query()
+ validQuery := validOptions(QueryRequest{})
+ for key, _ := range q {
+ if _, ok := validQuery[key]; !ok {
+ return nil, errors.New("invalid query params")
+ }
+ }
// Parse query string.
buf, err := ioutil.ReadAll(r.Body)
@@ -875,6 +882,21 @@ func (h *Handler) readURLQueryRequest(r *http.Request) (*QueryRequest, error) {
}, nil
}
+// validOptions return all attributes of an interface with lower first character.
+func validOptions(v interface{}) map[string]bool {
+ validQuery := make(map[string]bool)
+ argsType := reflect.ValueOf(v).Type()
+
+ for i := 0; i < argsType.NumField(); i++ {
+ fieldName := argsType.Field(i).Name
+ chars := []rune(fieldName)
+ chars[0] = unicode.ToLower(chars[0])
+ fieldName = string(chars)
+ validQuery[fieldName] = true
+ }
+ return validQuery
+}
+
// writeQueryResponse writes the response from the executor to w.
func (h *Handler) writeQueryResponse(w http.ResponseWriter, r *http.Request, resp *QueryResponse) error {
if strings.Contains(r.Header.Get("Accept"), "application/x-protobuf") {
diff --git a/handler_test.go b/handler_test.go
index 6744e74ea..427ad043e 100644
--- a/handler_test.go
+++ b/handler_test.go
@@ -209,6 +209,16 @@ func TestHandler_Query_Args_Err(t *testing.T) {
t.Fatalf("unexpected body: %q", body)
}
}
+func TestHandler_Query_Params_Err(t *testing.T) {
+ w := httptest.NewRecorder()
+ NewHandler().ServeHTTP(w, MustNewHTTPRequest("POST", "/index/idx0/query?slices=0,1&db=sample", strings.NewReader("Bitmap(id=100)")))
+ if w.Code != http.StatusBadRequest {
+ t.Fatalf("unexpected status code: %d", w.Code)
+ } else if body := w.Body.String(); body != `{"error":"invalid query params"}`+"\n" {
+ t.Fatalf("unexpected body: %q", body)
+ }
+
+}
// Ensure the handler can execute a query with a uint64 response as JSON.
func TestHandler_Query_Uint64_JSON(t *testing.T) {
diff --git a/pilosa.go b/pilosa.go
index a192364ed..13ade19ea 100644
--- a/pilosa.go
+++ b/pilosa.go
@@ -49,10 +49,10 @@ var (
)
// Regular expression to validate index and frame names.
-var nameRegexp = regexp.MustCompile(`^[a-z][a-z0-9_-]{0,64}$`)
+var nameRegexp = regexp.MustCompile(`^[a-z][a-z0-9_-]{0,63}$`)
// Regular expression to validate row and column labels.
-var labelRegexp = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]{0,64}$`)
+var labelRegexp = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]{0,63}$`)
// ColumnAttrSet represents a set of attributes for a vertical column in an index.
// Can have a set of attributes attached to it.
diff --git a/pilosa_test.go b/pilosa_test.go
new file mode 100644
index 000000000..ad5f9b762
--- /dev/null
+++ b/pilosa_test.go
@@ -0,0 +1,55 @@
+package pilosa_test
+
+import (
+ "testing"
+
+ "github.com/pilosa/pilosa"
+)
+
+func TestValidateName(t *testing.T) {
+ names := []string{
+ "a", "ab", "ab1", "b-c", "d_e",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ }
+ for _, name := range names {
+ if pilosa.ValidateName(name) != nil {
+ t.Fatalf("Should be valid index name: %s", name)
+ }
+ }
+}
+
+func TestValidateNameInvalid(t *testing.T) {
+ names := []string{
+ "", "'", "^", "/", "\\", "A", "*", "a:b", "valid?no", "yüce", "1", "_", "-",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1",
+ }
+ for _, name := range names {
+ if pilosa.ValidateName(name) == nil {
+ t.Fatalf("Should be invalid index name: %s", name)
+ }
+ }
+}
+
+func TestValidateLabel(t *testing.T) {
+ labels := []string{
+ "a", "ab", "ab1", "d_e", "A", "Bc", "B1", "aB", "b-c",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ }
+ for _, label := range labels {
+ if pilosa.ValidateLabel(label) != nil {
+ t.Fatalf("Should be valid label: %s", label)
+ }
+ }
+}
+
+func TestValidateLabelInvalid(t *testing.T) {
+ labels := []string{
+ "", "1", "_", "-", "'", "^", "/", "\\", "*", "a:b", "valid?no", "yüce",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1",
+ }
+ for _, label := range labels {
+ if pilosa.ValidateLabel(label) == nil {
+ t.Fatalf("Should be invalid label: %s", label)
+ }
+ }
+}
diff --git a/pql/ast.go b/pql/ast.go
index cf404ad12..13a989b03 100644
--- a/pql/ast.go
+++ b/pql/ast.go
@@ -72,7 +72,7 @@ func (c *Call) UintArg(key string) (uint64, bool, error) {
case uint64:
return tval, true, nil
default:
- return 0, true, fmt.Errorf("could not convert %v of type %T to uint64 in Calll.UintArg", tval, tval)
+ return 0, true, fmt.Errorf("could not convert %v of type %T to uint64 in Call.UintArg", tval, tval)
}
}
diff --git a/roaring/roaring.go b/roaring/roaring.go
index 21e87ed0f..03bd5488a 100644
--- a/roaring/roaring.go
+++ b/roaring/roaring.go
@@ -441,6 +441,39 @@ func (b *Bitmap) Difference(other *Bitmap) *Bitmap {
return output
}
+// Xor returns the bitwise exclusive or of b and other.
+func (b *Bitmap) Xor(other *Bitmap) *Bitmap {
+ output := &Bitmap{}
+
+ ki, ci := b.keys, b.containers
+ kj, cj := other.keys, other.containers
+
+ for {
+ var key uint64
+ var container *container
+
+ ni, nj := len(ki), len(kj)
+ if ni == 0 && nj == 0 { // eof(i,j)
+ break
+ } else if ni == 0 || (nj != 0 && ki[0] > kj[0]) { // eof(i) or i > j
+ key, container = kj[0], cj[0].clone()
+ kj, cj = kj[1:], cj[1:]
+ } else if nj == 0 || (ki[0] < kj[0]) { // eof(j) or i < j
+ key, container = ki[0], ci[0].clone()
+ ki, ci = ki[1:], ci[1:]
+ } else { // i == j
+ key, container = ki[0], xor(ci[0], cj[0])
+ ki, ci = ki[1:], ci[1:]
+ kj, cj = kj[1:], cj[1:]
+ }
+
+ output.keys = append(output.keys, key)
+ output.containers = append(output.containers, container)
+ }
+
+ return output
+}
+
// removeEmptyContainers deletes all containers that have a count of zero.
func (b *Bitmap) removeEmptyContainers() {
for i := 0; i < len(b.containers); {
@@ -1249,7 +1282,7 @@ func intersectionCountArrayArray(a, b *container) (n uint64) {
return n
}
-func intersectionCountArrayBitmap(a, b *container) (n uint64) {
+func intersectionCountArrayBitmapOld(a, b *container) (n uint64) {
// Copy array header so we can shrink it.
array := a.array
if len(array) == 0 {
@@ -1291,6 +1324,18 @@ func intersectionCountArrayBitmap(a, b *container) (n uint64) {
return n
}
+func intersectionCountArrayBitmap(a, b *container) (n uint64) {
+ for _, val := range a.array {
+ i := val / 64
+ if i >= uint32(len(b.bitmap)) {
+ break
+ }
+ off := val % 64
+ n += (b.bitmap[i] & (1 << off)) >> off
+ }
+ return n
+}
+
func intersectionCountBitmapBitmap(a, b *container) (n uint64) {
return popcntAndSlice(a.bitmap, b.bitmap)
}
@@ -1584,37 +1629,6 @@ func differenceBitmapBitmap(a, b *container) *container {
}
return output
}
-func (b *Bitmap) Xor(other *Bitmap) *Bitmap {
- output := &Bitmap{}
-
- ki, ci := b.keys, b.containers
- kj, cj := other.keys, other.containers
-
- for {
- var key uint64
- var container *container
-
- ni, nj := len(ki), len(kj)
- if ni == 0 && nj == 0 { // eof(i,j)
- break
- } else if ni == 0 || (nj != 0 && ki[0] > kj[0]) { // eof(i) or i > j
- key, container = kj[0], cj[0].clone()
- kj, cj = kj[1:], cj[1:]
- } else if nj == 0 || (ki[0] < kj[0]) { // eof(j) or i < j
- key, container = ki[0], ci[0].clone()
- ki, ci = ki[1:], ci[1:]
- } else { // i == j
- key, container = ki[0], xor(ci[0], cj[0])
- ki, ci = ki[1:], ci[1:]
- kj, cj = kj[1:], cj[1:]
- }
-
- output.keys = append(output.keys, key)
- output.containers = append(output.containers, container)
- }
-
- return output
-}
func xor(a, b *container) *container {
if a.isArray() {
@@ -1635,10 +1649,8 @@ func xor(a, b *container) *container {
func xorArrayArray(a, b *container) *container {
output := &container{}
na, nb := len(a.array), len(b.array)
- for i, j := 0, 0; ; {
- if i >= na && j >= nb {
- break
- } else if i < na && j >= nb {
+ for i, j := 0, 0; i < na || j < nb; {
+ if i < na && j >= nb {
output.add(a.array[i])
i++
continue
@@ -1664,32 +1676,19 @@ func xorArrayArray(a, b *container) *container {
}
func xorArrayBitmap(a, b *container) *container {
- output := &container{}
- itr := newBufIterator(newBitmapIterator(b.bitmap))
- for i := 0; ; {
- vb, eof := itr.next()
- if i >= len(a.array) && eof {
- break
- } else if i >= len(a.array) {
- output.add(vb)
- continue
- } else if eof {
- output.add(a.array[i])
- i++
- continue
- }
-
- va := a.array[i]
- if va < vb {
- output.add(va)
- i++
- itr.unread()
- } else if va > vb {
- output.add(vb)
+ output := b.clone()
+ for _, v := range a.array {
+ if b.bitmapContains(v) {
+ output.remove(v)
} else {
- i++
+ output.add(v)
}
}
+
+ if output.count() < ArrayMaxSize {
+ output.convertToArray()
+ }
+
return output
}
@@ -1703,6 +1702,10 @@ func xorBitmapBitmap(a, b *container) *container {
output.bitmap[i] = v
output.n += int(popcnt(v))
}
+
+ if output.count() < ArrayMaxSize {
+ output.convertToArray()
+ }
return output
}
diff --git a/roaring/roaring_internal_test.go b/roaring/roaring_internal_test.go
index 41d0d167e..caf760c74 100644
--- a/roaring/roaring_internal_test.go
+++ b/roaring/roaring_internal_test.go
@@ -14,9 +14,7 @@
package roaring
-import (
- "testing"
-)
+import "testing"
func TestBitmapCountRange(t *testing.T) {
c := container{}
@@ -41,3 +39,48 @@ func TestBitmapCountRange(t *testing.T) {
}
}
}
+
+func TestIntersectionCountArrayBitmap2(t *testing.T) {
+ a, b := &container{}, &container{}
+ tests := []struct {
+ array []uint32
+ bitmap []uint64
+ exp uint64
+ }{
+ {
+ array: []uint32{0},
+ bitmap: []uint64{1},
+ exp: 1,
+ },
+ {
+ array: []uint32{0, 1},
+ bitmap: []uint64{3},
+ exp: 2,
+ },
+ {
+ array: []uint32{64, 128, 129, 2000},
+ bitmap: []uint64{932421, 2},
+ exp: 0,
+ },
+ {
+ array: []uint32{0, 65, 130, 195},
+ bitmap: []uint64{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255},
+ exp: 4,
+ },
+ {
+ array: []uint32{63, 120, 543, 639, 12000},
+ bitmap: []uint64{0x8000000000000000, 0, 0, 0, 0, 0, 0, 0, 0, 0x8000000000000000},
+ exp: 2,
+ },
+ }
+
+ for i, test := range tests {
+ a.array = test.array
+ b.bitmap = test.bitmap
+ ret1 := intersectionCountArrayBitmapOld(a, b)
+ ret2 := intersectionCountArrayBitmap(a, b)
+ if ret1 != ret2 || ret2 != test.exp {
+ t.Fatalf("test #%v intersectCountArrayBitmap fail orig: %v new: %v exp: %v", i, ret1, ret2, test.exp)
+ }
+ }
+}
diff --git a/roaring/roaring_test.go b/roaring/roaring_test.go
index 619e194e2..3c2b832f4 100644
--- a/roaring/roaring_test.go
+++ b/roaring/roaring_test.go
@@ -137,8 +137,25 @@ func TestBitmap_Xor_ArrayArray(t *testing.T) {
if n := result.Count(); n != 2 {
t.Fatalf("unexpected n: %d", n)
}
+
+ //equivalence array test
+ result = result.Xor(result)
+ if n := result.Count(); n > 0 {
+ t.Fatalf("unexpected n: %d", n)
+ }
+
}
+//empty array test
+func TestBitmap_Xor_Empty(t *testing.T) {
+ bm1 := roaring.NewBitmap(0, 50000, 1000001, 1000002)
+ empty := roaring.NewBitmap()
+ result := bm1.Xor(empty)
+
+ if n := result.Count(); n != 4 {
+ t.Fatalf("unexpected n: %d", n)
+ }
+}
func TestBitmap_Xor_ArrayBitmap(t *testing.T) {
bm0 := roaring.NewBitmap(1, 70, 200, 4097, 4098)
bm1 := roaring.NewBitmap()
@@ -150,14 +167,28 @@ func TestBitmap_Xor_ArrayBitmap(t *testing.T) {
if n := result.Count(); n != 4999 {
t.Fatalf("unexpected n: %d", n)
}
+
+ //equivalence bitmap test
+ result = result.Xor(result)
+ if n := result.Count(); n > 0 {
+ t.Fatalf("unexpected n: %d", n)
+ }
+
+ empty := roaring.NewBitmap()
+ result = bm1.Xor(empty)
+ if n := result.Count(); n != 5000 {
+ t.Fatalf("unexpected n: %d", n)
+ }
}
func TestBitmap_Xor_BitmapBitmap(t *testing.T) {
bm0 := roaring.NewBitmap()
bm1 := roaring.NewBitmap()
+
for i := uint64(0); i < 10000; i += 2 {
bm1.Add(i)
}
+
for i := uint64(1); i < 10000; i += 2 {
bm0.Add(i)
}
diff --git a/server.go b/server.go
index 4f8f45653..fc8b3608f 100644
--- a/server.go
+++ b/server.go
@@ -101,7 +101,7 @@ func (s *Server) Open() error {
// Open HTTP listener to determine port (if specified as :0).
ln, err := net.Listen("tcp", ":"+port)
if err != nil {
- return err
+ return fmt.Errorf("net.Listen: %v", err)
}
s.ln = ln
@@ -115,16 +115,16 @@ func (s *Server) Open() error {
// Open holder.
if err := s.Holder.Open(); err != nil {
- return err
+ return fmt.Errorf("opening Holder: %v", err)
}
if err := s.BroadcastReceiver.Start(s); err != nil {
- return err
+ return fmt.Errorf("starting BroadcastReceiver: %v", err)
}
// Open NodeSet communication
if err := s.Cluster.NodeSet.Open(); err != nil {
- return err
+ return fmt.Errorf("opening NodeSet: %v", err)
}
// Create executor for executing queries.
diff --git a/webui/assets/main.js b/webui/assets/main.js
index 887181338..7927a3b42 100644
--- a/webui/assets/main.js
+++ b/webui/assets/main.js
@@ -1,8 +1,9 @@
class REPL {
- constructor(input, output, button) {
+ constructor(input, output, button, completer) {
this.input = input
this.output = output
this.button = button
+ this.completer = completer
this.history = []
this.history_index = 0
this.history_buffer = ''
@@ -16,21 +17,6 @@ class REPL {
UP_ARROW: 38,
DOWN_ARROW: 40
}
- var keywords = {
- // keyword: length of substring that comes after cursor
- "SetBit()": 1,
- "ClearBit()": 1,
- "SetRowAttrs()": 1,
- "SetColumnAttrs()": 1,
- "Bitmap()": 1,
- "Union()": 1,
- "Intersect()": 1,
- "Difference()": 1,
- "Count()": 1,
- "Range()": 1,
- "TopN()": 1,
- "frame=": 0,
- }
this.input.addEventListener("keydown", function(e) {
if (e.keyCode == keys.UP_ARROW) {
@@ -70,35 +56,7 @@ class REPL {
}
if (e.keyCode == keys.TAB) {
e.preventDefault()
-
- // extract word fragment ending at cursor. a word fragment:
- // - starts with last nonalpha character before cursor (or beginning of string)
- // - ends at cursor
- var word_start = repl.input.selectionEnd-1
- while(word_start>0) {
- var c = repl.input.value.charCodeAt(word_start)
- if(!((c>64 && c<91) || (c>96 && c<123))) {
- word_start++
- break
- }
- word_start--
- }
- var input_word = repl.input.value.substring(word_start, repl.input.selectionEnd)
-
- // check for keyword match and insert
- // this just stops at the first match
- for(var keyword in keywords) {
- if(keyword.startsWith(input_word)){
- var cursor_pos = repl.input.selectionEnd
- var completion = keyword.substring(input_word.length)
- var before = repl.input.value.substring(0, cursor_pos)
- var after = repl.input.value.substring(cursor_pos)
- repl.input.value = before + completion + after
- var new_pos = cursor_pos + completion.length - keywords[keyword]
- repl.input.setSelectionRange(new_pos, new_pos)
- break
- }
- }
+ repl.completer.complete()
}
})
repl.button.onclick = function() {
@@ -203,10 +161,10 @@ class REPL {
output_string += `
Just getting started? Try this:
- $ curl -XPOST "http://127.0.0.1:10101/index/test" -d '{"options": {"columnLabel": "col"}}' # create index "test"
- $ curl -XPOST "http://127.0.0.1:10101/index/test/frame/foo" -d '{"options": {"rowLabel": "row"}}' # create frame "foo"
- # Select "test" in the index dropdown above
- SetBit(row=0, col=0, frame=foo) # Use PQL to set a bit
+ :create index test
+ :use test
+ :create frame foo
+ SetBit(rowID=0, columnID=0, frame=foo) # Use PQL to set a bit
`
}
}
@@ -444,11 +402,94 @@ Date.prototype.timeNow = function () {
populate_version()
+
+class Autocompleter {
+ constructor(input, output) {
+ this.input = input
+ this.output = output
+ this.keyword_map = this.static_keywords
+ this.init_dynamic_keywords()
+ }
+
+ get static_keywords() {
+ return {
+ // keyword: length of substring that comes after cursor
+ "SetBit()": 1,
+ "ClearBit()": 1,
+ "SetRowAttrs()": 1,
+ "SetColumnAttrs()": 1,
+ "Bitmap()": 1,
+ "Union()": 1,
+ "Intersect()": 1,
+ "Difference()": 1,
+ "Count()": 1,
+ "Range()": 1,
+ "TopN()": 1,
+ "frame=": 0,
+ }
+ }
+
+ complete() {
+ var completer = this
+ // extract word fragment ending at cursor. a word fragment:
+ // - starts with last nonalpha character before cursor (or beginning of string)
+ // - ends at cursor
+ var word_start = completer.input.selectionEnd-1
+ while(word_start>0) {
+ var c = completer.input.value.charCodeAt(word_start)
+ if(!((c>64 && c<91) || (c>96 && c<123))) {
+ word_start++
+ break
+ }
+ word_start--
+ }
+ var input_word = completer.input.value.substring(word_start, completer.input.selectionEnd)
+
+ // check for keyword match and insert if exactly one match
+ var matches = []
+ for(var keyword in this.keyword_map) {
+ if(keyword.startsWith(input_word)){
+ matches.push(keyword)
+ }
+ }
+ if(matches.length > 1) {
+ // completer.output.innerHTML = whatever
+ }
+
+ if(matches.length == 1) {
+ // completer.output.innerHTML = ""
+ var cursor_pos = completer.input.selectionEnd
+ var completion = matches[0].substring(input_word.length)
+ var before = completer.input.value.substring(0, cursor_pos)
+ var after = completer.input.value.substring(cursor_pos)
+ completer.input.value = before + completion + after
+ var new_pos = cursor_pos + completion.length - this.keyword_map[matches[0]]
+ completer.input.setSelectionRange(new_pos, new_pos)
+ }
+ }
+
+ init_dynamic_keywords() {
+ // hit /schema, parse indexes, frames, rowlabels, columnlabels, add to list
+ }
+
+ add_keyword() {
+ // call when index or frame created in webui
+ }
+
+ remove_keyword() {
+ // call when index or frame deleted in webui
+ // issue: if e.g. multiple indexes have same frame, removing one removes all.
+ // solution: maintain count. requires more elaborate representation of keywords.
+ }
+}
+
var input = document.getElementById('query')
var output = document.getElementById('outputs')
var button = document.getElementById('query-btn')
+var autocomplete_output = document.getElementById('autocomplete-container')
-repl = new REPL(input, output, button)
+autocompleter = new Autocompleter(input, autocomplete_output)
+repl = new REPL(input, output, button, autocompleter)
repl.populate_index_dropdown()
repl.bind_events()
diff --git a/webui/assets/style.css b/webui/assets/style.css
index 3a5bcaf39..3567a2a03 100644
--- a/webui/assets/style.css
+++ b/webui/assets/style.css
@@ -287,12 +287,6 @@ td{
padding: 21px 30px;
}
-.string { color: green; }
-.number { color: darkorange; }
-.boolean { color: blue; }
-.null { color: magenta; }
-.key { color: red; }
-
.expand {
text-align: center;
}
diff --git a/webui/index.html b/webui/index.html
index 09e39d17a..dc06e310a 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -6,11 +6,11 @@