diff --git a/integration/update_field_compat_test.go b/integration/update_field_compat_test.go index ce73b7d2ed54..ea5101907d63 100644 --- a/integration/update_field_compat_test.go +++ b/integration/update_field_compat_test.go @@ -1262,10 +1262,51 @@ func TestUpdateFieldCompatBit(t *testing.T) { update: bson.D{{"$bit", bson.D{{"v", bson.D{{"and", "string"}}}}}}, resultType: emptyResult, }, + "Binary": { + update: bson.D{{"$bit", bson.D{{"v", bson.D{{"and", primitive.Binary{Subtype: 0x80, Data: []byte{42, 0, 13}}}}}}}}, + resultType: emptyResult, + }, + "ObjectID": { + update: bson.D{{"$bit", bson.D{{"v", bson.D{{"or", primitive.ObjectID{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11}}}}}}}, + resultType: emptyResult, + }, + "Bool": { + update: bson.D{{"$bit", bson.D{{"v", bson.D{{"or", true}}}}}}, + resultType: emptyResult, + }, + "DateTime": { + update: bson.D{{"$bit", bson.D{{"v", bson.D{{"or", primitive.NewDateTimeFromTime(time.Date(9999, 12, 31, 23, 59, 59, 999000000, time.UTC))}}}}}}, + resultType: emptyResult, + }, "Nil": { update: bson.D{{"$bit", bson.D{{"and", nil}}}}, resultType: emptyResult, }, + "Regex": { + update: bson.D{{"$bit", bson.D{{"v", bson.D{{"xor", primitive.Regex{Pattern: "foo", Options: "i"}}}}}}}, + resultType: emptyResult, + }, + "Timestamp": { + update: bson.D{{"$bit", bson.D{{"v", bson.D{{"xor", primitive.Timestamp{T: 42, I: 13}}}}}}}, + resultType: emptyResult, + }, + "Object": { + update: bson.D{{"$bit", bson.D{{"v", bson.D{{"xor", bson.D{{"foo", int32(42)}}}}}}}}, + resultType: emptyResult, + }, + "Array": { + update: bson.D{{"$bit", bson.D{{"v", bson.D{{"xor", bson.A{int32(42)}}}}}}}, + resultType: emptyResult, + }, + "NonExistent": { + update: bson.D{{"$bit", bson.D{{"non-existent", bson.D{{"xor", int32(1)}}}}}}, + }, + "DotNotation": { + update: bson.D{{"$bit", bson.D{{"v.foo", bson.D{{"xor", int32(1)}}}}}}, + }, + "DotNotationArray": { + update: bson.D{{"$bit", bson.D{{"v.0", bson.D{{"xor", int32(1)}}}}}}, + }, "DotNotationMissingField": { update: bson.D{{"$bit", bson.D{{"v..", int32(1)}}}}, resultType: emptyResult, diff --git a/internal/handlers/common/update.go b/internal/handlers/common/update.go index d454939ee373..591a59a72ac1 100644 --- a/internal/handlers/common/update.go +++ b/internal/handlers/common/update.go @@ -785,51 +785,21 @@ func processBitFieldExpression(command string, doc *types.Document, updateV any) ) } - path, err := types.NewPathFromString(bitKey) - if err != nil { - return false, lazyerrors.Error(err) - } - - var docValue any + // bitKey has valid path, checked in ValidateUpdateOperators + path := must.NotFail(types.NewPathFromString(bitKey)) - // $bit sets the field if it does not exist, then does bit operation using 0 and operand value. - hasKey := doc.HasByPath(path) - if !hasKey { - docValue = int32(0) - } else { - k := bitKey - if path.Len() > 1 { - k = path.Suffix() - } + // $bit sets the field if it does not exist by applying bitwise operat on 0 and operand value. + var docValue any = int32(0) - docValue, err = doc.GetByPath(path) - if err != nil { - return false, lazyerrors.Error(err) - } - - // $bit operations can only be performed on integer(int32/int64) fields - switch docValue.(type) { - case int32, int64: - default: - return false, newUpdateError( - commonerrors.ErrBadValue, - fmt.Sprintf( - `Cannot apply $bit to a value of non-integral type.`+ - `_id: %s has the field %s of non-integer type %s`, - types.FormatAnyValue(must.NotFail(doc.Get("_id"))), - k, - commonparams.AliasFromType(docValue), - ), - command, - ) - } + hasPath := doc.HasByPath(path) + if hasPath { + docValue = must.NotFail(doc.GetByPath(path)) } for _, bitOp := range nestedDoc.Keys() { bitOpValue := must.NotFail(nestedDoc.Get(bitOp)) - var bitOpResult any - bitOpResult, err = performBitLogic(bitOp, bitOpValue, docValue) + bitOpResult, err := performBitLogic(bitOp, bitOpValue, docValue) switch { case err == nil: @@ -837,7 +807,7 @@ func processBitFieldExpression(command string, doc *types.Document, updateV any) return false, newUpdateError(commonerrors.ErrUnsuitableValueType, err.Error(), command) } - if docValue == bitOpResult && hasKey { + if docValue == bitOpResult && hasPath { continue } @@ -862,11 +832,11 @@ func processBitFieldExpression(command string, doc *types.Document, updateV any) return false, newUpdateError( commonerrors.ErrBadValue, fmt.Sprintf( - `The $bit modifier field must be an Integer(32/64 bit); a `+ - `'%s' is not supported here: {%s: %s}`, + `Cannot apply $bit to a value of non-integral type.`+ + `_id: %s has the field %s of non-integer type %s`, + types.FormatAnyValue(must.NotFail(doc.Get("_id"))), + path.Suffix(), commonparams.AliasFromType(docValue), - bitOp, - types.FormatAnyValue(docValue), ), command, ) diff --git a/website/docs/reference/supported-commands.md b/website/docs/reference/supported-commands.md index cf380bccf6a0..cc9a7e10e424 100644 --- a/website/docs/reference/supported-commands.md +++ b/website/docs/reference/supported-commands.md @@ -116,7 +116,7 @@ The following operators and modifiers are available in the `update` and `findAnd | | `$position` | ⚠️ | [Issue](https://github.com/FerretDB/FerretDB/issues/829) | | | `$slice` | ⚠️ | [Issue](https://github.com/FerretDB/FerretDB/issues/830) | | | `$sort` | ⚠️ | [Issue](https://github.com/FerretDB/FerretDB/issues/831) | -| | `$bit` | ⚠️ | [Issue](https://github.com/FerretDB/FerretDB/issues/821) | +| | `$bit` | ✅️ | | ### Projection Operators