Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support $rename field update operator #1753

Merged
merged 48 commits into from
Jan 13, 2023
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
7a26bae
wip
noisersup Jan 4, 2023
8b9641d
wip
noisersup Jan 4, 2023
fa48763
wip
noisersup Jan 5, 2023
f9ee246
wip
noisersup Jan 5, 2023
7409e97
wip
noisersup Jan 5, 2023
d29931a
wip
noisersup Jan 5, 2023
f3d6fa8
wip
noisersup Jan 5, 2023
d1e3b3d
wip
noisersup Jan 5, 2023
3635d3a
wip
noisersup Jan 5, 2023
fc3e97e
wip
noisersup Jan 5, 2023
d94217f
wip
noisersup Jan 6, 2023
0b7bf27
wip
noisersup Jan 6, 2023
a942027
wip
noisersup Jan 6, 2023
baa4a5e
wip
noisersup Jan 6, 2023
c417def
Merge branch 'main' into rename-update-operator-626
noisersup Jan 6, 2023
f7af8c9
wip
noisersup Jan 6, 2023
882fbe7
wip
noisersup Jan 6, 2023
215b49c
Merge branch 'main' into rename-update-operator-626
mergify[bot] Jan 6, 2023
3d2e4d0
Merge branch 'main' into rename-update-operator-626
mergify[bot] Jan 6, 2023
1ce59e4
wip
noisersup Jan 9, 2023
d89d436
wip
noisersup Jan 9, 2023
addbc3e
Merge branch 'main' into rename-update-operator-626
mergify[bot] Jan 9, 2023
5a6b250
wip
noisersup Jan 10, 2023
e511305
wip
noisersup Jan 10, 2023
18ecb80
Merge branch 'main' into rename-update-operator-626
mergify[bot] Jan 10, 2023
9edd94e
wip
noisersup Jan 10, 2023
8a32051
wip
noisersup Jan 10, 2023
e69b24c
wip
noisersup Jan 10, 2023
58107c6
wip
noisersup Jan 10, 2023
04c8057
Update internal/types/path.go
noisersup Jan 11, 2023
6176b19
wip
noisersup Jan 11, 2023
d55a9f7
wip
noisersup Jan 11, 2023
38ec522
wip
noisersup Jan 11, 2023
a99608c
wip
noisersup Jan 11, 2023
55c72ba
wip
noisersup Jan 11, 2023
c944c71
wip
noisersup Jan 11, 2023
bf21a30
wip
noisersup Jan 11, 2023
0f1aa02
wip
noisersup Jan 11, 2023
d4b5d85
wip
noisersup Jan 11, 2023
0f25e59
wip
noisersup Jan 11, 2023
b970deb
wip
noisersup Jan 12, 2023
e2268bb
wip
noisersup Jan 12, 2023
fab6690
Add suggestions from reviews
noisersup Jan 12, 2023
d0c3a32
remove obsolete check
noisersup Jan 12, 2023
b18a5df
Merge branch 'main' into rename-update-operator-626
noisersup Jan 12, 2023
b35521c
Merge branch 'main' into rename-update-operator-626
mergify[bot] Jan 12, 2023
16e054e
Merge branch 'main' into rename-update-operator-626
Jan 12, 2023
168ce98
Merge branch 'main' into rename-update-operator-626
AlekSi Jan 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ tasks:
cmds:
- >
go test -count=1 {{.RACEFLAG}} -tags={{.BUILDTAGS}} -shuffle=on -coverpkg=../...
-timeout=15m
noisersup marked this conversation as resolved.
Show resolved Hide resolved
-coverprofile=integration-tigris.txt .
-handler=tigris
-records-dir=../records
Expand Down
1 change: 1 addition & 0 deletions integration/shareddata/scalars.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ func tigrisSchema(typeString string) string {
"primary_key": ["_id"],
"properties": {
"v": {%%type%%},
"boo": {%%type%%},
"_id": {"type": "string"}
}
}`
Expand Down
74 changes: 74 additions & 0 deletions integration/update_field_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,80 @@ func TestUpdateFieldCompatMin(t *testing.T) {
testUpdateCompat(t, testCases)
}

func TestUpdateFieldCompatRename(t *testing.T) {
testCases := map[string]updateCompatTestCase{
rumyantseva marked this conversation as resolved.
Show resolved Hide resolved
"Simple": {
update: bson.D{{"$rename", bson.D{{"v", "foo"}}}},
},
"DuplicateField": {
update: bson.D{{"$rename", bson.D{{"v", "v"}}}},
resultType: emptyResult,
},
"NonExistingField": {
update: bson.D{{"$rename", bson.D{{"foo", "bar"}}}},
resultType: emptyResult,
},
"EmptyField": {
update: bson.D{{"$rename", bson.D{{"", "v"}}}},
resultType: emptyResult,
},
"EmptyDest": {
update: bson.D{{"$rename", bson.D{{"v", ""}}}},
resultType: emptyResult,
},
"DotDocumentMove": {
update: bson.D{{"$rename", bson.D{{"v.foo", "boo"}}}},
skipForTigris: "https://github.com/FerretDB/FerretDB/issues/1776",
},
"DotDocumentDuplicate": {
update: bson.D{{"$rename", bson.D{{"v.foo", "v.array"}}}},
skipForTigris: "https://github.com/FerretDB/FerretDB/issues/1776",
},
"DotDocumentNonExisting": {
update: bson.D{{"$rename", bson.D{{"foo.bar", ""}}}},
resultType: emptyResult,
},
"DotArrayField": {
update: bson.D{{"$rename", bson.D{{"v.array.0", ""}}}},
resultType: emptyResult,
},
"DotArrayNonExisting": {
update: bson.D{{"$rename", bson.D{{"foo.0.baz", int32(1)}}}},
resultType: emptyResult,
},
"Multiple": {
update: bson.D{{"$rename", bson.D{{"v.foo", "v.bar"}, {"v.42", "v.43"}}}},
skipForTigris: "https://github.com/FerretDB/FerretDB/issues/1776",
noisersup marked this conversation as resolved.
Show resolved Hide resolved
},
"MultipleConflictDestSource": {
update: bson.D{{"$rename", bson.D{{"v", "foo"}, {"foo", "bar"}}}},
resultType: emptyResult,
},
"MultipleConflictDestFields": {
update: bson.D{{"$rename", bson.D{{"v", "foo"}, {"v", "bar"}}}},
resultType: emptyResult,
},
"MultipleSecondInvalid": {
update: bson.D{{"$rename", bson.D{{"v.foo", "boo"}, {"v.array", 1}}}},
resultType: emptyResult,
},
"FieldEmpty": {
update: bson.D{{"$rename", bson.D{}}},
resultType: emptyResult,
},
"InvalidString": {
update: bson.D{{"$rename", "string"}},
resultType: emptyResult,
},
"InvalidDoc": {
update: bson.D{{"$rename", primitive.D{}}},
resultType: emptyResult,
},
}

testUpdateCompat(t, testCases)
}

func TestUpdateFieldCompatUnset(t *testing.T) {
t.Parallel()

Expand Down
3 changes: 3 additions & 0 deletions internal/handlers/common/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ const (
// ErrInvalidID indicates that _id field is invalid.
ErrInvalidID = ErrorCode(53) // InvalidID

// ErrEmptyName indicates that the field name is empty.
ErrEmptyName = ErrorCode(56) // EmptyName

// ErrCommandNotFound indicates unknown command input.
ErrCommandNotFound = ErrorCode(59) // CommandNotFound

Expand Down
42 changes: 22 additions & 20 deletions internal/handlers/common/errorcode_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

146 changes: 145 additions & 1 deletion internal/handlers/common/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package common

import (
"errors"
"fmt"
"math"
"sort"
Expand All @@ -24,6 +25,7 @@ import (
"golang.org/x/exp/slices"

"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/internal/util/iterator"
"github.com/FerretDB/FerretDB/internal/util/must"
)

Expand Down Expand Up @@ -103,6 +105,12 @@ func UpdateDocument(doc, update *types.Document) (bool, error) {
return false, err
}

case "$rename":
changed, err = processRenameFieldExpression(doc, updateV.(*types.Document))
if err != nil {
return false, err
}

default:
if strings.HasPrefix(updateOp, "$") {
return false, NewCommandError(ErrNotImplemented, fmt.Errorf("UpdateDocument: unhandled operation %q", updateOp))
Expand Down Expand Up @@ -236,6 +244,63 @@ func processPopFieldExpression(doc *types.Document, update *types.Document) (boo
return changed, nil
}

// processRenameFieldExpression changes document according to $rename operator.
// If the document was changed it returns true.
func processRenameFieldExpression(doc *types.Document, update *types.Document) (bool, error) {
renameExpression := update.SortFieldsByKey()

var changed bool

for _, key := range renameExpression.Keys() {
renameRawValue := must.NotFail(renameExpression.Get(key))

if key == "" || renameRawValue == "" {
return changed, NewWriteErrorMsg(ErrEmptyName, "An empty update path is not valid.")
}

// this is covered in validateRenameExpression
renameValue := renameRawValue.(string)

if key == renameRawValue {
return changed, NewWriteErrorMsg(
ErrBadValue,
rumyantseva marked this conversation as resolved.
Show resolved Hide resolved
fmt.Sprintf("The source and target field for $rename must differ: %s: %#v", key, renameValue),
)
}

sourcePath := types.NewPathFromString(key)
targetPath := types.NewPathFromString(renameValue)

// Get value to move
val, err := doc.GetByPath(sourcePath)
if err != nil {
var dpe *types.DocumentPathError
if !errors.As(err, &dpe) {
panic("getByPath returned error with invalid type")
rumyantseva marked this conversation as resolved.
Show resolved Hide resolved
}

if dpe.Code() == types.ErrDocumentPathKeyNotFound {
continue
}

return changed, NewWriteErrorMsg(ErrUnsuitableValueType, dpe.Error())
}

// Remove old document
doc.RemoveByPath(sourcePath)

// Set new path with old value
err = doc.SetByPath(targetPath, val)
if err != nil {
return changed, err
}

changed = true
noisersup marked this conversation as resolved.
Show resolved Hide resolved
}

return changed, nil
}

// processIncFieldExpression changes document according to $inc operator.
// If the document was changed it returns true.
func processIncFieldExpression(doc *types.Document, updateV any) (bool, error) {
Expand Down Expand Up @@ -522,13 +587,23 @@ func ValidateUpdateOperators(update *types.Document) error {
return err
}

_, err = extractValueFromUpdateOperator("$rename", update)
if err != nil {
return err
}

if err = checkConflictingChanges(set, inc); err != nil {
return err
}

if err = validateCurrentDateExpression(update); err != nil {
return err
}

if err = validateRenameExpression(update); err != nil {
return err
}

return nil
}

Expand All @@ -552,8 +627,10 @@ func HasSupportedUpdateModifiers(update *types.Document) (bool, error) {
case "$unset":
fallthrough
case "$pop":
fallthrough
case "$rename":
updateModifier = true
case "$mul", "$rename":
case "$mul":
return false, NewCommandErrorMsgWithArgument(
ErrNotImplemented,
fmt.Sprintf("update operator %s is not implemented", updateOp),
Expand Down Expand Up @@ -611,6 +688,8 @@ func checkConflictingChanges(a, b *types.Document) error {
// The result returned for "$setOnInsert" operator is
//
// bson.D{{"v", nil}}.
//
// It also checks for path collisions and returns the error if there's any.
w84thesun marked this conversation as resolved.
Show resolved Hide resolved
func extractValueFromUpdateOperator(op string, update *types.Document) (*types.Document, error) {
if !update.Has(op) {
return nil, nil
Expand All @@ -635,6 +714,71 @@ func extractValueFromUpdateOperator(op string, update *types.Document) (*types.D
return doc, nil
}

// validateRenameExpression validates $rename input on correctness.
func validateRenameExpression(update *types.Document) error {
if !update.Has("$rename") {
return nil
}

updateExpression := must.NotFail(update.Get("$rename"))

doc, ok := updateExpression.(*types.Document)
if !ok {
return NewWriteErrorMsg(ErrFailedToParse, "Modifiers operate on fields but we found another type instead")
}

iter := doc.Iterator()
keys := map[string]struct{}{}

for {
k, v, err := iter.Next()
if err != nil {
if errors.Is(err, iterator.ErrIteratorDone) {
break
}

return err
rumyantseva marked this conversation as resolved.
Show resolved Hide resolved
}

var vStr string

vStr, ok = v.(string)
if !ok {
return NewWriteErrorMsg(
ErrBadValue,
fmt.Sprintf("The 'to' field for $rename must be a string: %s: %v", k, vStr),
)
}

if k == vStr {
return NewWriteErrorMsg(
ErrBadValue,
fmt.Sprintf("The source and target field for $rename must differ: %s: %v", k, vStr),
)
}

if _, ok = keys[k]; ok {
return NewWriteErrorMsg(
ErrConflictingUpdateOperators,
fmt.Sprintf("Updating the '%s' would create a conflict at '%s'", k, k),
)
}

keys[k] = struct{}{}

if _, ok = keys[vStr]; ok {
return NewWriteErrorMsg(
rumyantseva marked this conversation as resolved.
Show resolved Hide resolved
ErrConflictingUpdateOperators,
fmt.Sprintf("Updating the '%s' would create a conflict at '%s'", vStr, vStr),
)
}

keys[vStr] = struct{}{}
}

return nil
}

// validateCurrentDateExpression validates $currentDate input on correctness.
func validateCurrentDateExpression(update *types.Document) error {
currentDateTopField, err := update.Get("$currentDate")
Expand Down
Loading