diff --git a/executor.go b/executor.go index 473df14b5..414528be7 100644 --- a/executor.go +++ b/executor.go @@ -2941,38 +2941,41 @@ func (e *executor) executeSet(ctx context.Context, index string, c *pql.Call, op } } - // Int field. - if f.Type() == FieldTypeInt || f.Type() == FieldTypeDecimal { + switch f.Type() { + case FieldTypeInt, FieldTypeDecimal: + // Int or Decimal field. + v, ok := c.Arg(fieldName) + if !ok { + return false, fmt.Errorf("Set() row argument '%v' required", rowLabel) + } // Read row value. - rowVal, ok, err := c.IntArg(fieldName) + rowVal, err := getScaledInt(f, v) + if err != nil { + return false, fmt.Errorf("reading Set() row: %v", err) + } + return e.executeSetValueField(ctx, index, c, f, colID, rowVal, opt) + + default: + // Read row ID. + rowID, ok, err := c.UintArg(fieldName) if err != nil { return false, fmt.Errorf("reading Set() row: %v", err) } else if !ok { return false, fmt.Errorf("Set() row argument '%v' required", rowLabel) } - return e.executeSetValueField(ctx, index, c, f, colID, rowVal, opt) - } - - // Read row ID. - rowID, ok, err := c.UintArg(fieldName) - if err != nil { - return false, fmt.Errorf("reading Set() row: %v", err) - } else if !ok { - return false, fmt.Errorf("Set() row argument '%v' required", rowLabel) - } - - var timestamp *time.Time - sTimestamp, ok := c.Args["_timestamp"].(string) - if ok { - t, err := time.Parse(TimeFormat, sTimestamp) - if err != nil { - return false, fmt.Errorf("invalid date: %s", sTimestamp) + var timestamp *time.Time + sTimestamp, ok := c.Args["_timestamp"].(string) + if ok { + t, err := time.Parse(TimeFormat, sTimestamp) + if err != nil { + return false, fmt.Errorf("invalid date: %s", sTimestamp) + } + timestamp = &t } - timestamp = &t - } - return e.executeSetBitField(ctx, index, c, f, colID, rowID, timestamp, opt) + return e.executeSetBitField(ctx, index, c, f, colID, rowID, timestamp, opt) + } } // executeSetBitField executes a Set() call for a specific field. @@ -4448,8 +4451,10 @@ func getCondIntSlice(f *Field, cond *pql.Condition) ([]int64, error) { // the field type. func getScaledInt(f *Field, v interface{}) (int64, error) { var value int64 - if f.Options().Type == FieldTypeDecimal { - scale := f.Options().Scale + + opt := f.Options() + if opt.Type == FieldTypeDecimal { + scale := opt.Scale switch tv := v.(type) { case int64: value = int64(float64(tv) * math.Pow10(int(scale))) diff --git a/executor_test.go b/executor_test.go index 7586da42f..fa66f0acb 100644 --- a/executor_test.go +++ b/executor_test.go @@ -766,6 +766,63 @@ func TestExecutor_Execute_SetBool(t *testing.T) { }) } +// Ensure a set query can be executed on a decimal field. +func TestExecutor_Execute_SetDecimal(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + hldr := test.Holder{Holder: c[0].Server.Holder()} + + // Create fields. + index := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{}) + if _, err := index.CreateFieldIfNotExists("f", pilosa.OptFieldTypeDecimal(2)); err != nil { + t.Fatal(err) + } + + // Set a value. + if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Set(1000, f=1.5)`}); err != nil { + t.Fatal(err) + } else if !res.Results[0].(bool) { + t.Fatalf("expected column changed") + } + + // Set the same value again verify nothing changed. + if res, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Set(1000, f=1.5)`}); err != nil { + t.Fatal(err) + } else if res.Results[0].(bool) { + t.Fatalf("expected column to be unchanged") + } + + if result, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Row(f == 1.5)`}); err != nil { + t.Fatal(err) + } else if columns := result.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(columns, []uint64{1000}) { + t.Fatalf("unexpected colums: %+v", columns) + } + + if result, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Row(f > 1.4999)`}); err != nil { + t.Fatal(err) + } else if columns := result.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(columns, []uint64{1000}) { + t.Fatalf("unexpected colums: %+v", columns) + } + }) + t.Run("Error", func(t *testing.T) { + c := test.MustRunCluster(t, 1) + defer c.Close() + hldr := test.Holder{Holder: c[0].Server.Holder()} + + // Create fields. + index := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{}) + if _, err := index.CreateFieldIfNotExists("f", pilosa.OptFieldTypeDecimal(2)); err != nil { + t.Fatal(err) + } + + // Set decimal using a string value. + if _, err := c[0].API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Set(1000, f="1.5")`}); err == nil { + t.Fatalf("expected invalid decimal type error") + } + }) +} + // Ensure old PQL syntax doesn't break anything too badly. func TestExecutor_Execute_OldPQL(t *testing.T) { c := test.MustRunCluster(t, 1) diff --git a/pql/ast.go b/pql/ast.go index 9855f5b96..1f87a170d 100644 --- a/pql/ast.go +++ b/pql/ast.go @@ -557,6 +557,13 @@ func (c *Call) CallIndex() string { return "" } +// Arg is for reading the value at key from call.Args. +// If the key is not in Call.Args, the value of the returned bool will be false. +func (c *Call) Arg(key string) (interface{}, bool) { + v, ok := c.Args[key] + return v, ok +} + // BoolArg is for reading the value at key from call.Args as a bool. If the // key is not in Call.Args, the value of the returned bool will be false, and // the error will be nil. The value is assumed to be a bool. An error is