diff --git a/executor.go b/executor.go index f83c0cbf3..7c23ac6e4 100644 --- a/executor.go +++ b/executor.go @@ -2006,6 +2006,8 @@ func (e *executor) translateCall(index string, idx *Index, c *pql.Call) error { } else if c.Name == "Rows" { fieldName = callArgString(c, "field") rowKey = "previous" + } else if c.Name == "GroupBy" { + return errors.Wrap(e.translateGroupByCall(index, idx, c), "translating GroupBy") } else { colKey = "col" fieldName = callArgString(c, "field") @@ -2067,6 +2069,61 @@ func (e *executor) translateCall(index string, idx *Index, c *pql.Call) error { return nil } +func (e *executor) translateGroupByCall(index string, idx *Index, c *pql.Call) error { + if c.Name != "GroupBy" { + panic("translateGroupByCall called with '" + c.Name + "'") + } + + for _, child := range c.Children { + if err := e.translateCall(index, idx, child); err != nil { + return errors.Wrapf(err, "translating %s", child) + } + } + + prev, ok := c.Args["previous"] + if !ok { + return nil // nothing else to be translated + } + previous, ok := prev.([]interface{}) + if !ok { + return errors.Errorf("'previous' argument must be list, but got %T", prev) + } + if len(c.Children) != len(previous) { + return errors.Errorf("mismatched lengths for previous: %d and children: %d in %s", len(previous), len(c.Children), c) + } + + fields := make([]*Field, len(c.Children)) + for i, child := range c.Children { + fieldname := callArgString(child, "field") + field := idx.Field(fieldname) + if field == nil { + return errors.Wrapf(ErrFieldNotFound, "getting field '%s' from '%s'", fieldname, child) + } + fields[i] = field + } + + for i, field := range fields { + prev := previous[i] + if field.keys() { + prevStr, ok := prev.(string) + if !ok { + return errors.New("prev value must be a string when field 'keys' option enabled") + } + ids, err := e.TranslateStore.TranslateRowsToUint64(index, field.Name(), []string{prevStr}) + if err != nil { + return errors.Wrapf(err, "translating row key '%s'", prevStr) + } + previous[i] = ids[0] + } else { + if prevStr, ok := prev.(string); ok { + return errors.Errorf("got string row val '%s' in 'previous' for field %s which doesn't use string keys", prevStr, field.Name()) + } + } + + } + return nil +} + func (e *executor) translateResult(index string, idx *Index, call *pql.Call, result interface{}) (interface{}, error) { switch result := result.(type) { case *Row: diff --git a/executor_internal_test.go b/executor_internal_test.go new file mode 100644 index 000000000..e90948561 --- /dev/null +++ b/executor_internal_test.go @@ -0,0 +1,122 @@ +package pilosa + +import ( + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/pilosa/pilosa/pql" +) + +func TestExecutor_TranslateGroupByCall(t *testing.T) { + e := &executor{ + Holder: NewHolder(), + } + e.Holder.Path, _ = ioutil.TempDir("", "") + err := e.Holder.Open() + if err != nil { + t.Fatalf("opening holder: %v", err) + } + + e.TranslateStore = e.Holder.translateFile + tf, _ := ioutil.TempFile("", "") + e.Holder.translateFile.Path = tf.Name() + err = e.Holder.translateFile.Open() + if err != nil { + t.Fatalf("opening translateFile: %v", err) + } + + idx, err := e.Holder.CreateIndex("i", IndexOptions{}) + if err != nil { + t.Fatalf("creating index: %v", err) + } + + _, erra := idx.CreateField("ak", OptFieldKeys()) + _, errb := idx.CreateField("b") + _, errc := idx.CreateField("ck", OptFieldKeys()) + if erra != nil || errb != nil || errc != nil { + t.Fatalf("creating fields %v, %v, %v", erra, errb, errc) + } + + _, erra = e.TranslateStore.TranslateRowsToUint64("i", "ak", []string{"la"}) + _, errb = e.TranslateStore.TranslateRowsToUint64("i", "ck", []string{"ha"}) + if erra != nil || errb != nil { + t.Fatalf("translating rows %v, %v", erra, errb) + } + + query, err := pql.ParseString(`GroupBy(Rows(field=ak), Rows(field=b), Rows(field=ck), previous=["la", 0, "ha"])`) + if err != nil { + t.Fatalf("parsing query: %v", err) + } + c := query.Calls[0] + err = e.translateGroupByCall("i", idx, c) + if err != nil { + t.Fatalf("translating call: %v", err) + } + if len(c.Args["previous"].([]interface{})) != 3 { + t.Fatalf("unexpected length for 'previous' arg %v", c.Args["previous"]) + } + for i, v := range c.Args["previous"].([]interface{}) { + if !isInt(v) { + t.Fatalf("expected all items in previous to be ints, but '%v' at index %d is %[1]T", v, i) + } + } + + errTests := []struct { + pql string + err string + }{ + { + pql: `GroupBy(Rows(field=notfound), previous=1)`, + err: "'previous' argument must be list", + }, + { + pql: `GroupBy(Rows(field=ak), previous=["la", 0])`, + err: "mismatched lengths", + }, + { + pql: `GroupBy(Rows(field=ak), previous=[1])`, + err: "prev value must be a string", + }, + { + pql: `GroupBy(Rows(field=notfound), previous=[1])`, + err: ErrFieldNotFound.Error(), + }, + // TODO: an unknown key will actually allocate an id. this is probably bad. + // { + // pql: `GroupBy(Rows(field=ak), previous=["zoop"])`, + // err: "translating row key '", + // }, + { + pql: `GroupBy(Rows(field=b), previous=["la"])`, + err: "which doesn't use string keys", + }, + } + + for i, test := range errTests { + t.Run(fmt.Sprintf("#%d_%s", i, test.err), func(t *testing.T) { + query, err := pql.ParseString(test.pql) + if err != nil { + t.Fatalf("parsing query: %v", err) + } + c := query.Calls[0] + err = e.translateGroupByCall("i", idx, c) + if err == nil { + t.Fatalf("expected error, but translated call is '%s", c) + } + if !strings.Contains(err.Error(), test.err) { + t.Fatalf("expected '%s', got '%v'", test.err, err) + } + }) + } +} + +func isInt(a interface{}) bool { + switch a.(type) { + case int, int64, uint, uint64: + return true + default: + return false + } +}