From 03765a221809a46cdb17be8d1067666621d3754d Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 7 Apr 2023 11:29:58 +0100 Subject: [PATCH 01/46] WIP --- internal/handlers/common/projection.go | 76 ++++++++++++-------------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 4b9c0ee1bc2d..5795144049c0 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -15,34 +15,16 @@ package common import ( + "errors" "fmt" + "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/util/iterator" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" "github.com/FerretDB/FerretDB/internal/util/must" ) -// ProjectDocuments modifies given documents in places according to the given projection. -func ProjectDocuments(docs []*types.Document, projection *types.Document) error { - if projection.Len() == 0 { - return nil - } - - inclusion, err := isProjectionInclusion(projection) - if err != nil { - return err - } - - for i := 0; i < len(docs); i++ { - err = projectDocument(inclusion, docs[i], projection) - if err != nil { - return err - } - } - - return nil -} - // isProjectionInclusion: projection can be only inclusion or exclusion. Validate and return true if inclusion. // Exception for the _id field. func isProjectionInclusion(projection *types.Document) (inclusion bool, err error) { @@ -55,8 +37,8 @@ func isProjectionInclusion(projection *types.Document) (inclusion bool, err erro switch v := v.(type) { case *types.Document: for _, projectionType := range v.Keys() { - err = NewCommandError( - ErrNotImplemented, + err = commonerrors.NewCommandError( + commonerrors.ErrNotImplemented, fmt.Errorf("projection of %s is not supported", projectionType), ) @@ -67,7 +49,8 @@ func isProjectionInclusion(projection *types.Document) (inclusion bool, err erro result := types.Compare(v, int32(0)) if result == types.Equal { if inclusion { - err = NewCommandErrorMsgWithArgument(ErrProjectionExIn, + err = commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrProjectionExIn, fmt.Sprintf("Cannot do exclusion on field %s in inclusion projection", k), "projection", ) @@ -76,7 +59,8 @@ func isProjectionInclusion(projection *types.Document) (inclusion bool, err erro exclusion = true } else { if exclusion { - err = NewCommandErrorMsgWithArgument(ErrProjectionInEx, + err = commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrProjectionInEx, fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", k), "projection", ) @@ -88,7 +72,8 @@ func isProjectionInclusion(projection *types.Document) (inclusion bool, err erro case bool: if v { if exclusion { - err = NewCommandErrorMsgWithArgument(ErrProjectionInEx, + err = commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrProjectionInEx, fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", k), "projection", ) @@ -97,7 +82,8 @@ func isProjectionInclusion(projection *types.Document) (inclusion bool, err erro inclusion = true } else { if inclusion { - err = NewCommandErrorMsgWithArgument(ErrProjectionExIn, + err = commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrProjectionExIn, fmt.Sprintf("Cannot do exclusion on field %s in inclusion projection", k), "projection", ) @@ -115,16 +101,26 @@ func isProjectionInclusion(projection *types.Document) (inclusion bool, err erro } func projectDocument(inclusion bool, doc *types.Document, projection *types.Document) error { - projectionMap := projection.Map() + iter := doc.Iterator() + defer iter.Close() + + for { + key, _, err := iter.Next() + if errors.Is(err, iterator.ErrIteratorDone) { + break + } - for k1 := range doc.Map() { - projectionVal, ok := projectionMap[k1] - if !ok { - if k1 == "_id" { // if _id is not in projection map, do not do anything with it + if err != nil { + return lazyerrors.Error(err) + } + + projectionVal, err := projection.Get(key) + if err != nil { + if key == "_id" { // if _id is not in projection map, do not do anything with it continue } if inclusion { // k1 from doc is absent in projection, remove from doc only if projection type inclusion - doc.Remove(k1) + doc.Remove(key) } continue } @@ -138,16 +134,16 @@ func projectDocument(inclusion bool, doc *types.Document, projection *types.Docu case float64, int32, int64: // field: number result := types.Compare(projectionVal, int32(0)) if result == types.Equal { - doc.Remove(k1) + doc.Remove(key) } case bool: // field: bool if !projectionVal { - doc.Remove(k1) + doc.Remove(key) } default: - return lazyerrors.Errorf("unsupported operation %s %v (%T)", k1, projectionVal, projectionVal) + return lazyerrors.Errorf("unsupported operation %s %v (%T)", key, projectionVal, projectionVal) } } return nil @@ -157,13 +153,13 @@ func applyComplexProjection(projectionVal *types.Document) error { for _, projectionType := range projectionVal.Keys() { switch projectionType { case "$elemMatch", "$slice": - return NewCommandError( - ErrNotImplemented, + return commonerrors.NewCommandError( + commonerrors.ErrNotImplemented, fmt.Errorf("projection of %s is not supported", projectionType), ) default: - return NewCommandError( - ErrCommandNotFound, + return commonerrors.NewCommandError( + commonerrors.ErrCommandNotFound, fmt.Errorf("projection of %s is not supported", projectionType), ) } From e649ddffa78be02786e1d72ea9553e510bf35049 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 7 Apr 2023 11:45:33 +0100 Subject: [PATCH 02/46] WIP --- integration/query_compat_command_test.go | 2 + integration/query_projection_compat_test.go | 17 +++++ integration/query_projection_test.go | 60 ------------------ internal/handlers/common/projection.go | 69 +++++++++++---------- 4 files changed, 55 insertions(+), 93 deletions(-) delete mode 100644 integration/query_projection_test.go diff --git a/integration/query_compat_command_test.go b/integration/query_compat_command_test.go index a54b891a49b5..bbdb4622f297 100644 --- a/integration/query_compat_command_test.go +++ b/integration/query_compat_command_test.go @@ -169,6 +169,8 @@ func testQueryCompatCommand(t *testing.T, testCases map[string]queryCompatComman } func TestQueryCompatCommandSkip(t *testing.T) { + t.Parallel() + testCases := map[string]queryCompatCommandTestCase{ "MaxInt64": { filter: bson.D{}, diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index fa6bbf92a1e0..1089126464ed 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -15,6 +15,7 @@ package integration import ( + "github.com/FerretDB/FerretDB/integration/setup" "testing" "go.mongodb.org/mongo-driver/bson" @@ -40,3 +41,19 @@ func TestQueryProjectionCompat(t *testing.T) { testQueryCompat(t, testCases) } + +func TestQueryProjectionCompatCommand(t *testing.T) { + setup.SkipForTigris(t) + + t.Parallel() + + testCases := map[string]queryCompatCommandTestCase{ + "FindProjectionIDExclusion": { + filter: bson.D{{"_id", "document-composite"}}, + projection: bson.D{{"_id", false}, {"array", int32(1)}}, + resultPushdown: true, + }, + } + + testQueryCompatCommand(t, testCases) +} diff --git a/integration/query_projection_test.go b/integration/query_projection_test.go deleted file mode 100644 index aa3460cf455f..000000000000 --- a/integration/query_projection_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package integration - -import ( - "testing" - - "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo/options" - - "github.com/FerretDB/FerretDB/integration/setup" - "github.com/FerretDB/FerretDB/integration/shareddata" -) - -func TestQueryProjection(t *testing.T) { - setup.SkipForTigris(t) - - t.Parallel() - ctx, collection := setup.Setup(t, shareddata.Composites) - - for name, tc := range map[string]struct { - projection any - filter any - expected bson.D - }{ - "FindProjectionIDExclusion": { - filter: bson.D{{"_id", "document-composite"}}, - // TODO: https://github.com/FerretDB/FerretDB/issues/537 - projection: bson.D{{"_id", false}, {"array", int32(1)}}, - expected: bson.D{}, - }, - } { - name, tc := name, tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - cursor, err := collection.Find(ctx, tc.filter, options.Find().SetProjection(tc.projection)) - require.NoError(t, err) - - var actual []bson.D - err = cursor.All(ctx, &actual) - require.NoError(t, err) - require.Len(t, actual, 1) - AssertEqualDocuments(t, tc.expected, actual[0]) - }) - } -} diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 5795144049c0..aee509948068 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -17,87 +17,90 @@ package common import ( "errors" "fmt" - "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" - "github.com/FerretDB/FerretDB/internal/util/iterator" + "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/iterator" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" - "github.com/FerretDB/FerretDB/internal/util/must" ) // isProjectionInclusion: projection can be only inclusion or exclusion. Validate and return true if inclusion. // Exception for the _id field. -func isProjectionInclusion(projection *types.Document) (inclusion bool, err error) { +func isProjectionInclusion(projection *types.Document) (bool, error) { var exclusion bool - for _, k := range projection.Keys() { - if k == "_id" { // _id is a special case and can be both + var inclusion bool + + iter := projection.Iterator() + for { + key, value, err := iter.Next() + if errors.Is(err, iterator.ErrIteratorDone) { + break + } + + if err != nil { + return false, lazyerrors.Error(err) + } + + if key == "_id" { // _id is a special case and can be both continue } - v := must.NotFail(projection.Get(k)) - switch v := v.(type) { - case *types.Document: - for _, projectionType := range v.Keys() { - err = commonerrors.NewCommandError( - commonerrors.ErrNotImplemented, - fmt.Errorf("projection of %s is not supported", projectionType), - ) - return - } + switch value := value.(type) { + case *types.Document: + return false, commonerrors.NewCommandErrorMsg( + commonerrors.ErrNotImplemented, + "projection expressions is not supported", + ) case float64, int32, int64: - result := types.Compare(v, int32(0)) + result := types.Compare(value, int32(0)) if result == types.Equal { if inclusion { - err = commonerrors.NewCommandErrorMsgWithArgument( + return false, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrProjectionExIn, - fmt.Sprintf("Cannot do exclusion on field %s in inclusion projection", k), + fmt.Sprintf("Cannot do exclusion on field %s in inclusion projection", key), "projection", ) - return } exclusion = true } else { if exclusion { - err = commonerrors.NewCommandErrorMsgWithArgument( + return false, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrProjectionInEx, - fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", k), + fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", key), "projection", ) - return } inclusion = true } case bool: - if v { + if value { if exclusion { - err = commonerrors.NewCommandErrorMsgWithArgument( + return false, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrProjectionInEx, - fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", k), + fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", key), "projection", ) - return } inclusion = true } else { if inclusion { - err = commonerrors.NewCommandErrorMsgWithArgument( + return false, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrProjectionExIn, - fmt.Sprintf("Cannot do exclusion on field %s in inclusion projection", k), + fmt.Sprintf("Cannot do exclusion on field %s in inclusion projection", key), "projection", ) - return } exclusion = true } default: - err = lazyerrors.Errorf("unsupported operation %s %v (%T)", k, v, v) - return + return false, lazyerrors.Errorf("unsupported operation %s %value (%T)", key, value, value) } } - return + + return inclusion, nil } func projectDocument(inclusion bool, doc *types.Document, projection *types.Document) error { From 9f2b3b086baef09d344c878c1eb56f1011cf4a94 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 7 Apr 2023 11:56:37 +0100 Subject: [PATCH 03/46] WIP --- internal/handlers/common/projection.go | 45 +++++++------------ .../handlers/common/projection_iterator.go | 4 +- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index aee509948068..a9f90170767e 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -31,6 +31,8 @@ func isProjectionInclusion(projection *types.Document) (bool, error) { var inclusion bool iter := projection.Iterator() + defer iter.Close() + for { key, value, err := iter.Next() if errors.Is(err, iterator.ErrIteratorDone) { @@ -103,10 +105,12 @@ func isProjectionInclusion(projection *types.Document) (bool, error) { return inclusion, nil } -func projectDocument(inclusion bool, doc *types.Document, projection *types.Document) error { +func projectDocument(inclusion bool, doc *types.Document, projection *types.Document) (*types.Document, error) { iter := doc.Iterator() defer iter.Close() + projected := doc.DeepCopy() + for { key, _, err := iter.Next() if errors.Is(err, iterator.ErrIteratorDone) { @@ -114,7 +118,7 @@ func projectDocument(inclusion bool, doc *types.Document, projection *types.Docu } if err != nil { - return lazyerrors.Error(err) + return nil, lazyerrors.Error(err) } projectionVal, err := projection.Get(key) @@ -123,50 +127,35 @@ func projectDocument(inclusion bool, doc *types.Document, projection *types.Docu continue } if inclusion { // k1 from doc is absent in projection, remove from doc only if projection type inclusion - doc.Remove(key) + projected.Remove(key) } continue } switch projectionVal := projectionVal.(type) { // found in the projection case *types.Document: // field: { $elemMatch: { field2: value }} - if err := applyComplexProjection(projectionVal); err != nil { - return err - } + return nil, commonerrors.NewCommandErrorMsg( + commonerrors.ErrCommandNotFound, + fmt.Sprintf("projection %s is not supported", + types.FormatAnyValue(projectionVal), + ), + ) case float64, int32, int64: // field: number result := types.Compare(projectionVal, int32(0)) if result == types.Equal { - doc.Remove(key) + projected.Remove(key) } case bool: // field: bool if !projectionVal { - doc.Remove(key) + projected.Remove(key) } default: - return lazyerrors.Errorf("unsupported operation %s %v (%T)", key, projectionVal, projectionVal) - } - } - return nil -} - -func applyComplexProjection(projectionVal *types.Document) error { - for _, projectionType := range projectionVal.Keys() { - switch projectionType { - case "$elemMatch", "$slice": - return commonerrors.NewCommandError( - commonerrors.ErrNotImplemented, - fmt.Errorf("projection of %s is not supported", projectionType), - ) - default: - return commonerrors.NewCommandError( - commonerrors.ErrCommandNotFound, - fmt.Errorf("projection of %s is not supported", projectionType), - ) + return nil, lazyerrors.Errorf("unsupported operation %s %v (%T)", key, projectionVal, projectionVal) } } - return nil + return projected, nil } diff --git a/internal/handlers/common/projection_iterator.go b/internal/handlers/common/projection_iterator.go index e68444dd0fdb..213ba0b683bd 100644 --- a/internal/handlers/common/projection_iterator.go +++ b/internal/handlers/common/projection_iterator.go @@ -54,12 +54,12 @@ func (iter *projectionIterator) Next() (struct{}, *types.Document, error) { return unused, nil, lazyerrors.Error(err) } - err = projectDocument(iter.inclusion, doc, iter.projection) + projected, err := projectDocument(iter.inclusion, doc, iter.projection) if err != nil { return unused, nil, lazyerrors.Error(err) } - return unused, doc, nil + return unused, projected, nil } // Close implements iterator.Interface. See ProjectionIterator for details. From 2a064bd596d732f9143a6aa54a5f8f8e822b3ead Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 7 Apr 2023 13:09:11 +0100 Subject: [PATCH 04/46] WIP --- integration/query_projection_compat_test.go | 12 ++++++++++++ internal/handlers/common/projection.go | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index 1089126464ed..3408aed844e0 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -37,6 +37,18 @@ func TestQueryProjectionCompat(t *testing.T) { skipForTigris: "Tigris does not support language keyword 'array' as field name", resultPushdown: true, }, + "IncludeField": { + filter: bson.D{}, + projection: bson.D{{"v", int32(1)}}, + }, + "ExcludeField": { + filter: bson.D{}, + projection: bson.D{{"v", int32(0)}}, + }, + "DotNotationIncludeExclude": { + filter: bson.D{}, + projection: bson.D{{"v.foo", true}, {"v.array", false}}, + }, } testQueryCompat(t, testCases) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index a9f90170767e..dcff6f9b848d 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -106,6 +106,10 @@ func isProjectionInclusion(projection *types.Document) (bool, error) { } func projectDocument(inclusion bool, doc *types.Document, projection *types.Document) (*types.Document, error) { + if projection == nil { + return doc, nil + } + iter := doc.Iterator() defer iter.Close() From 50214fd66bbea0c8b6f54ff53c5489ae529cdcc7 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 7 Apr 2023 14:20:41 +0100 Subject: [PATCH 05/46] WIP --- internal/handlers/common/projection.go | 92 ++++++++----------- .../handlers/common/projection_iterator.go | 3 +- 2 files changed, 41 insertions(+), 54 deletions(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index dcff6f9b848d..0e140fcd98ed 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -24,11 +24,11 @@ import ( "github.com/FerretDB/FerretDB/internal/util/lazyerrors" ) -// isProjectionInclusion: projection can be only inclusion or exclusion. Validate and return true if inclusion. -// Exception for the _id field. -func isProjectionInclusion(projection *types.Document) (bool, error) { - var exclusion bool - var inclusion bool +// validateProjection check projection document. +// Document fields could be either included or excluded but not both. +// Exception is for the _id field that could be included or excluded. +func validateProjection(projection *types.Document) error { + var projectionVal *bool iter := projection.Iterator() defer iter.Close() @@ -40,69 +40,59 @@ func isProjectionInclusion(projection *types.Document) (bool, error) { } if err != nil { - return false, lazyerrors.Error(err) + return lazyerrors.Error(err) } - if key == "_id" { // _id is a special case and can be both + if key == "_id" { // _id is a special case and can be included or excluded continue } + var result bool + switch value := value.(type) { case *types.Document: - return false, commonerrors.NewCommandErrorMsg( + return commonerrors.NewCommandErrorMsg( commonerrors.ErrNotImplemented, "projection expressions is not supported", ) case float64, int32, int64: - result := types.Compare(value, int32(0)) - if result == types.Equal { - if inclusion { - return false, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrProjectionExIn, - fmt.Sprintf("Cannot do exclusion on field %s in inclusion projection", key), - "projection", - ) - } - exclusion = true - } else { - if exclusion { - return false, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrProjectionInEx, - fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", key), - "projection", - ) - } - inclusion = true + comparison := types.Compare(value, int32(0)) + if comparison != types.Equal { + result = true } - case bool: if value { - if exclusion { - return false, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrProjectionInEx, - fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", key), - "projection", - ) - } - inclusion = true - } else { - if inclusion { - return false, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrProjectionExIn, - fmt.Sprintf("Cannot do exclusion on field %s in inclusion projection", key), - "projection", - ) - } - exclusion = true + result = true } - default: - return false, lazyerrors.Errorf("unsupported operation %s %value (%T)", key, value, value) + return lazyerrors.Errorf("unsupported operation %s %value (%T)", key, value, value) + } + + // if projectionVal is nil, it means that we are processing the first field + if projectionVal == nil { + projectionVal = &result + continue + } + + if *projectionVal != result { + if *projectionVal { + return commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrProjectionExIn, + fmt.Sprintf("Cannot do exclusion on field %s in inclusion projection", key), + "projection", + ) + } else { + return commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrProjectionInEx, + fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", key), + "projection", + ) + } } } - return inclusion, nil + return nil } func projectDocument(inclusion bool, doc *types.Document, projection *types.Document) (*types.Document, error) { @@ -127,12 +117,10 @@ func projectDocument(inclusion bool, doc *types.Document, projection *types.Docu projectionVal, err := projection.Get(key) if err != nil { - if key == "_id" { // if _id is not in projection map, do not do anything with it + if key == "_id" { continue } - if inclusion { // k1 from doc is absent in projection, remove from doc only if projection type inclusion - projected.Remove(key) - } + continue } diff --git a/internal/handlers/common/projection_iterator.go b/internal/handlers/common/projection_iterator.go index 213ba0b683bd..6ac70f44fb06 100644 --- a/internal/handlers/common/projection_iterator.go +++ b/internal/handlers/common/projection_iterator.go @@ -26,7 +26,7 @@ import ( // Close method closes the underlying iterator. // For that reason, there is no need to track both iterators. func ProjectionIterator(iter types.DocumentsIterator, projection *types.Document) (types.DocumentsIterator, error) { - inclusion, err := isProjectionInclusion(projection) + err := validateProjection(projection) if err != nil { return nil, lazyerrors.Error(err) } @@ -34,7 +34,6 @@ func ProjectionIterator(iter types.DocumentsIterator, projection *types.Document return &projectionIterator{ iter: iter, projection: projection, - inclusion: inclusion, }, nil } From bc49a2e5fe493302d1ddfd7e1415ccc286af0ba3 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 7 Apr 2023 14:22:09 +0100 Subject: [PATCH 06/46] WIP --- internal/handlers/common/projection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 0e140fcd98ed..83a10c2cb45f 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -53,7 +53,7 @@ func validateProjection(projection *types.Document) error { case *types.Document: return commonerrors.NewCommandErrorMsg( commonerrors.ErrNotImplemented, - "projection expressions is not supported", + fmt.Sprintf("projection expression %s is not supported", types.FormatAnyValue(value)), ) case float64, int32, int64: From b93246a0cade303798493592715b9e91ee2a131a Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 7 Apr 2023 14:24:11 +0100 Subject: [PATCH 07/46] WIP --- integration/query_projection_compat_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index 3408aed844e0..7b07b971d77c 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -15,7 +15,6 @@ package integration import ( - "github.com/FerretDB/FerretDB/integration/setup" "testing" "go.mongodb.org/mongo-driver/bson" @@ -55,8 +54,6 @@ func TestQueryProjectionCompat(t *testing.T) { } func TestQueryProjectionCompatCommand(t *testing.T) { - setup.SkipForTigris(t) - t.Parallel() testCases := map[string]queryCompatCommandTestCase{ From d02d0120debbe58b00f6c6807aa4546177b254f2 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 7 Apr 2023 16:59:57 +0100 Subject: [PATCH 08/46] WIP --- integration/query_projection_compat_test.go | 9 ++++ internal/handlers/common/projection.go | 48 ++++++++++--------- .../handlers/common/projection_iterator.go | 2 +- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index 7b07b971d77c..26af1584d577 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -44,9 +44,18 @@ func TestQueryProjectionCompat(t *testing.T) { filter: bson.D{}, projection: bson.D{{"v", int32(0)}}, }, + "DotNotationInclude": { + filter: bson.D{}, + projection: bson.D{{"v.foo", true}, {"v.array", true}}, + }, + "DotNotationExclude": { + filter: bson.D{}, + projection: bson.D{{"v.foo", false}, {"v.array", false}}, + }, "DotNotationIncludeExclude": { filter: bson.D{}, projection: bson.D{{"v.foo", true}, {"v.array", false}}, + resultType: emptyResult, }, } diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 83a10c2cb45f..3e010c42df5d 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -22,6 +22,7 @@ import ( "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/iterator" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" ) // validateProjection check projection document. @@ -69,6 +70,9 @@ func validateProjection(projection *types.Document) error { return lazyerrors.Errorf("unsupported operation %s %value (%T)", key, value, value) } + // set the value with boolean result to omit type assertion in the next iteration + projection.Set(key, result) + // if projectionVal is nil, it means that we are processing the first field if projectionVal == nil { projectionVal = &result @@ -95,18 +99,20 @@ func validateProjection(projection *types.Document) error { return nil } -func projectDocument(inclusion bool, doc *types.Document, projection *types.Document) (*types.Document, error) { +func projectDocument(doc *types.Document, projection *types.Document) (*types.Document, error) { if projection == nil { return doc, nil } - iter := doc.Iterator() - defer iter.Close() + projected := types.MakeDocument(1) + + projected.Set("_id", must.NotFail(doc.Get("_id"))) - projected := doc.DeepCopy() + iter := projection.Iterator() + defer iter.Close() for { - key, _, err := iter.Next() + key, value, err := iter.Next() if errors.Is(err, iterator.ErrIteratorDone) { break } @@ -115,37 +121,33 @@ func projectDocument(inclusion bool, doc *types.Document, projection *types.Docu return nil, lazyerrors.Error(err) } - projectionVal, err := projection.Get(key) + path, err := types.NewPathFromString(key) if err != nil { - if key == "_id" { - continue - } - - continue + return nil, lazyerrors.Error(err) } - switch projectionVal := projectionVal.(type) { // found in the projection + switch value := value.(type) { // found in the projection case *types.Document: // field: { $elemMatch: { field2: value }} return nil, commonerrors.NewCommandErrorMsg( commonerrors.ErrCommandNotFound, fmt.Sprintf("projection %s is not supported", - types.FormatAnyValue(projectionVal), + types.FormatAnyValue(value), ), ) - case float64, int32, int64: // field: number - result := types.Compare(projectionVal, int32(0)) - if result == types.Equal { - projected.Remove(key) - } - case bool: // field: bool - if !projectionVal { - projected.Remove(key) + // if projection value is false, we should skip the field + if !value { + projected.RemoveByPath(path) + continue } - default: - return nil, lazyerrors.Errorf("unsupported operation %s %v (%T)", key, projectionVal, projectionVal) + return nil, lazyerrors.Errorf("unsupported operation %s %v (%T)", key, value, value) + } + + // if doc has field set it to the projected document + if doc.HasByPath(path) { + projected.SetByPath(path, must.NotFail(doc.GetByPath(path))) } } diff --git a/internal/handlers/common/projection_iterator.go b/internal/handlers/common/projection_iterator.go index 6ac70f44fb06..643af374db6e 100644 --- a/internal/handlers/common/projection_iterator.go +++ b/internal/handlers/common/projection_iterator.go @@ -53,7 +53,7 @@ func (iter *projectionIterator) Next() (struct{}, *types.Document, error) { return unused, nil, lazyerrors.Error(err) } - projected, err := projectDocument(iter.inclusion, doc, iter.projection) + projected, err := projectDocument(doc, iter.projection) if err != nil { return unused, nil, lazyerrors.Error(err) } From 2fd9a422c1fc74a65947f6b1233dad11ecf61aa4 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 12 Apr 2023 09:52:36 +0100 Subject: [PATCH 09/46] WIP --- internal/handlers/common/projection.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index a31a9f2a9c79..183035f81d7a 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -143,7 +143,7 @@ func projectDocument(doc *types.Document, projection *types.Document) (*types.Do } default: - return nil, lazyerrors.Errorf("unsupported operation %s %v (%T)", k1, projectionVal, projectionVal) + return nil, lazyerrors.Errorf("unsupported operation %s %v (%T)", key, value, value) } // if doc has field set it to the projected document @@ -152,5 +152,5 @@ func projectDocument(doc *types.Document, projection *types.Document) (*types.Do } } - return projected, nil + return projected, nil } From 4fd99585bb2ef842631a7538a70de6605e98c90e Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 12 Apr 2023 12:40:58 +0100 Subject: [PATCH 10/46] WIP --- integration/shareddata/shareddata.go | 54 ++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/integration/shareddata/shareddata.go b/integration/shareddata/shareddata.go index 1256a6108c7f..b664643f3c7c 100644 --- a/integration/shareddata/shareddata.go +++ b/integration/shareddata/shareddata.go @@ -197,6 +197,60 @@ func (values *Values[idType]) IsCompatible(backend string) bool { return slices.Contains(values.backends, backend) } +// fields is a map of field name -> value. +type fields map[string]any + +// TopLevelValues stores shared data documents as {"_id": key, "field1": value1, "field2": value2, ...} documents. +type TopLevelValues[id comparable] struct { + name string + backends []string + validators map[string]map[string]any // backend -> validator name -> validator + data map[id]fields +} + +// Name implements Provider interface. +func (t *TopLevelValues[id]) Name() string { + return t.name +} + +// Validators implements Provider interface. +func (t *TopLevelValues[id]) Validators(backend, collection string) map[string]any { + switch backend { + case "ferretdb-tigris": + validators := make(map[string]any, len(t.validators[backend])) + for key, value := range t.validators[backend] { + validators[key] = strings.ReplaceAll(value.(string), "%%collection%%", collection) + } + return validators + default: + return t.validators[backend] + } +} + +// Docs implements Provider interface. +func (t *TopLevelValues[id]) Docs() []bson.D { + ids := maps.Keys(t.data) + + res := make([]bson.D, 0, len(t.data)) + for _, id := range ids { + doc := bson.D{{"_id", id}} + fields := t.data[id] + if fields != nil { + for key, field := range fields { + doc = append(doc, bson.E{Key: key, Value: field}) + } + } + res = append(res, doc) + } + + return res +} + +// IsCompatible implements Provider interface. +func (t *TopLevelValues[id]) IsCompatible(backend string) bool { + return slices.Contains(t.backends, backend) +} + // check interfaces var ( _ Provider = (*Values[string])(nil) From 8bde6653f10fec0500f2bdca0914d93cb5583f0f Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 12 Apr 2023 14:10:56 +0100 Subject: [PATCH 11/46] WIP --- integration/query_projection_compat_test.go | 12 ++++------ integration/shareddata/scalars.go | 24 +++++++++++++++++++ integration/shareddata/shareddata.go | 2 ++ internal/handlers/common/projection.go | 3 ++- .../handlers/common/projection_iterator.go | 1 - 5 files changed, 32 insertions(+), 10 deletions(-) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index 26af1584d577..a5fe38f56a1f 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -25,16 +25,12 @@ func TestQueryProjectionCompat(t *testing.T) { testCases := map[string]queryCompatTestCase{ "FindProjectionInclusions": { - filter: bson.D{{"_id", "document-composite"}}, - projection: bson.D{{"foo", int32(1)}, {"42", true}}, - skipForTigris: "Tigris does not support field names started from numbers (`42`)", - resultPushdown: true, + filter: bson.D{}, + projection: bson.D{{"foo", int32(1)}, {"bar", true}}, }, "FindProjectionExclusions": { - filter: bson.D{{"_id", "document-composite"}}, - projection: bson.D{{"foo", int32(0)}, {"array", false}}, - skipForTigris: "Tigris does not support language keyword 'array' as field name", - resultPushdown: true, + filter: bson.D{}, + projection: bson.D{{"foo", int32(0)}, {"bar", false}}, }, "IncludeField": { filter: bson.D{}, diff --git a/integration/shareddata/scalars.go b/integration/shareddata/scalars.go index 8c639a383a38..77dbadd44697 100644 --- a/integration/shareddata/scalars.go +++ b/integration/shareddata/scalars.go @@ -434,6 +434,30 @@ var ObjectIDKeys = &Values[primitive.ObjectID]{ }, } +var TopLevelFieldsIntegers = &TopLevelValues[string]{ + name: "TopLevelFieldsIntegers", + backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + validators: map[string]map[string]any{ + "ferretdb-tigris": { + "$tigrisSchemaString": `{ + "title": "%%collection%%", + "primary_key": ["_id"], + "properties": { + "foo": {"type": "integer", "format": "int32"}, + "bar": {"type": "integer", "format": "int32"}, + "_id": {"type": "string"} + } + }`, + }, + }, + data: map[string]fields{ + "int32-two": { + "foo": int32(1), + "bar": int32(2), + }, + }, +} + // tigrisSchema returns a tigris schema for the given type. // In addition to the "v" field, it also sets "foo" field with the same type, so it can be used // for test cases where multiple fields need to be supported (for example, $rename). diff --git a/integration/shareddata/shareddata.go b/integration/shareddata/shareddata.go index b664643f3c7c..3bc12f781b5f 100644 --- a/integration/shareddata/shareddata.go +++ b/integration/shareddata/shareddata.go @@ -81,6 +81,8 @@ func AllProviders() Providers { Mixed, // TODO https://github.com/FerretDB/FerretDB/issues/2291 // ArrayAndDocuments, + + TopLevelFieldsIntegers, } // check that names are unique and randomize order diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 183035f81d7a..98d3c83ccc3c 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -104,7 +104,8 @@ func projectDocument(doc *types.Document, projection *types.Document) (*types.Do return doc, nil } - projected := types.MakeDocument(1) + var projected *types.Document + projected = types.MakeDocument(1) projected.Set("_id", must.NotFail(doc.Get("_id"))) diff --git a/internal/handlers/common/projection_iterator.go b/internal/handlers/common/projection_iterator.go index 643af374db6e..94ffcb148f4e 100644 --- a/internal/handlers/common/projection_iterator.go +++ b/internal/handlers/common/projection_iterator.go @@ -41,7 +41,6 @@ func ProjectionIterator(iter types.DocumentsIterator, projection *types.Document type projectionIterator struct { iter types.DocumentsIterator projection *types.Document - inclusion bool } // Next implements iterator.Interface. See ProjectionIterator for details. From fa1097ca58e33dedb4c3d06bb0c210ad731e9dcd Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 12 Apr 2023 14:40:05 +0100 Subject: [PATCH 12/46] WIP --- internal/handlers/common/projection.go | 24 +++++++++++-------- .../handlers/common/projection_iterator.go | 6 +++-- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 98d3c83ccc3c..93d61358801a 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -28,7 +28,7 @@ import ( // validateProjection check projection document. // Document fields could be either included or excluded but not both. // Exception is for the _id field that could be included or excluded. -func validateProjection(projection *types.Document) error { +func validateProjection(projection *types.Document) (bool, error) { var projectionVal *bool iter := projection.Iterator() @@ -41,7 +41,7 @@ func validateProjection(projection *types.Document) error { } if err != nil { - return lazyerrors.Error(err) + return false, lazyerrors.Error(err) } if key == "_id" { // _id is a special case and can be included or excluded @@ -52,7 +52,7 @@ func validateProjection(projection *types.Document) error { switch value := value.(type) { case *types.Document: - return commonerrors.NewCommandErrorMsg( + return false, commonerrors.NewCommandErrorMsg( commonerrors.ErrNotImplemented, fmt.Sprintf("projection expression %s is not supported", types.FormatAnyValue(value)), ) @@ -67,7 +67,7 @@ func validateProjection(projection *types.Document) error { result = true } default: - return lazyerrors.Errorf("unsupported operation %s %value (%T)", key, value, value) + return false, lazyerrors.Errorf("unsupported operation %s %value (%T)", key, value, value) } // set the value with boolean result to omit type assertion in the next iteration @@ -81,13 +81,13 @@ func validateProjection(projection *types.Document) error { if *projectionVal != result { if *projectionVal { - return commonerrors.NewCommandErrorMsgWithArgument( + return false, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrProjectionExIn, fmt.Sprintf("Cannot do exclusion on field %s in inclusion projection", key), "projection", ) } else { - return commonerrors.NewCommandErrorMsgWithArgument( + return false, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrProjectionInEx, fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", key), "projection", @@ -96,18 +96,22 @@ func validateProjection(projection *types.Document) error { } } - return nil + return *projectionVal == true, nil } -func projectDocument(doc *types.Document, projection *types.Document) (*types.Document, error) { +func projectDocument(doc *types.Document, projection *types.Document, inclusion bool) (*types.Document, error) { if projection == nil { return doc, nil } var projected *types.Document - projected = types.MakeDocument(1) + if inclusion { + projected = types.MakeDocument(1) - projected.Set("_id", must.NotFail(doc.Get("_id"))) + projected.Set("_id", must.NotFail(doc.Get("_id"))) + } else { + projected = doc.DeepCopy() + } iter := projection.Iterator() defer iter.Close() diff --git a/internal/handlers/common/projection_iterator.go b/internal/handlers/common/projection_iterator.go index 94ffcb148f4e..8dbc2254386a 100644 --- a/internal/handlers/common/projection_iterator.go +++ b/internal/handlers/common/projection_iterator.go @@ -26,7 +26,7 @@ import ( // Close method closes the underlying iterator. // For that reason, there is no need to track both iterators. func ProjectionIterator(iter types.DocumentsIterator, projection *types.Document) (types.DocumentsIterator, error) { - err := validateProjection(projection) + inclusion, err := validateProjection(projection) if err != nil { return nil, lazyerrors.Error(err) } @@ -34,6 +34,7 @@ func ProjectionIterator(iter types.DocumentsIterator, projection *types.Document return &projectionIterator{ iter: iter, projection: projection, + inclusion: inclusion, }, nil } @@ -41,6 +42,7 @@ func ProjectionIterator(iter types.DocumentsIterator, projection *types.Document type projectionIterator struct { iter types.DocumentsIterator projection *types.Document + inclusion bool } // Next implements iterator.Interface. See ProjectionIterator for details. @@ -52,7 +54,7 @@ func (iter *projectionIterator) Next() (struct{}, *types.Document, error) { return unused, nil, lazyerrors.Error(err) } - projected, err := projectDocument(doc, iter.projection) + projected, err := projectDocument(doc, iter.projection, iter.inclusion) if err != nil { return unused, nil, lazyerrors.Error(err) } From 96580d35d83a40e718fd14bf5e3e1bab9544c6ec Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 13 Apr 2023 14:02:38 +0100 Subject: [PATCH 13/46] WIP --- internal/handlers/common/projection.go | 13 +++++++++---- internal/handlers/common/projection_iterator.go | 5 +++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 93d61358801a..9114e42bf9c3 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -25,10 +25,19 @@ import ( "github.com/FerretDB/FerretDB/internal/util/must" ) +var ( + // errProjectionEmpty is returned when projection document is empty. + errProjectionEmpty = errors.New("projection is empty") +) + // validateProjection check projection document. // Document fields could be either included or excluded but not both. // Exception is for the _id field that could be included or excluded. func validateProjection(projection *types.Document) (bool, error) { + if projection == nil { + return false, errProjectionEmpty + } + var projectionVal *bool iter := projection.Iterator() @@ -100,10 +109,6 @@ func validateProjection(projection *types.Document) (bool, error) { } func projectDocument(doc *types.Document, projection *types.Document, inclusion bool) (*types.Document, error) { - if projection == nil { - return doc, nil - } - var projected *types.Document if inclusion { projected = types.MakeDocument(1) diff --git a/internal/handlers/common/projection_iterator.go b/internal/handlers/common/projection_iterator.go index 8dbc2254386a..81c262fe9f11 100644 --- a/internal/handlers/common/projection_iterator.go +++ b/internal/handlers/common/projection_iterator.go @@ -15,6 +15,7 @@ package common import ( + "errors" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" ) @@ -27,6 +28,10 @@ import ( // For that reason, there is no need to track both iterators. func ProjectionIterator(iter types.DocumentsIterator, projection *types.Document) (types.DocumentsIterator, error) { inclusion, err := validateProjection(projection) + if errors.Is(err, errProjectionEmpty) { + return iter, nil + } + if err != nil { return nil, lazyerrors.Error(err) } From 850032ffba2a19aec1b4e908a8cb3ca83ead588c Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 13 Apr 2023 14:12:34 +0100 Subject: [PATCH 14/46] WIP --- internal/handlers/common/projection.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 9114e42bf9c3..b52a0d66a273 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -108,7 +108,8 @@ func validateProjection(projection *types.Document) (bool, error) { return *projectionVal == true, nil } -func projectDocument(doc *types.Document, projection *types.Document, inclusion bool) (*types.Document, error) { +// projectDocument applies projection to the copy of the document. +func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Document, error) { var projected *types.Document if inclusion { projected = types.MakeDocument(1) From 0d252676138d50d2d44571728fec75b4ccfe6c7b Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 13 Apr 2023 14:50:23 +0100 Subject: [PATCH 15/46] WIP --- integration/query_projection_compat_test.go | 4 ++++ internal/handlers/common/projection.go | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index a5fe38f56a1f..2fc42b7ed793 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -48,6 +48,10 @@ func TestQueryProjectionCompat(t *testing.T) { filter: bson.D{}, projection: bson.D{{"v.foo", false}, {"v.array", false}}, }, + "DotNotationExcludeSecondLevel": { + filter: bson.D{}, + projection: bson.D{{"v.array.42", false}}, + }, "DotNotationIncludeExclude": { filter: bson.D{}, projection: bson.D{{"v.foo", true}, {"v.array", false}}, diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index b52a0d66a273..57f17f5f51d7 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -79,10 +79,10 @@ func validateProjection(projection *types.Document) (bool, error) { return false, lazyerrors.Errorf("unsupported operation %s %value (%T)", key, value, value) } - // set the value with boolean result to omit type assertion in the next iteration + // set the value with boolean result to omit type assertion when we will apply projection projection.Set(key, result) - // if projectionVal is nil, it means that we are processing the first field + // if projectionVal is nil we are processing the first field if projectionVal == nil { projectionVal = &result continue From 307b7dc5dd4b5ee705839d816349eecc35e028b0 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 13 Apr 2023 15:28:30 +0100 Subject: [PATCH 16/46] WIP --- integration/query_projection_compat_test.go | 8 ++++++++ internal/handlers/common/projection.go | 20 ++++++++++--------- .../handlers/common/projection_iterator.go | 4 ++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index 2fc42b7ed793..4b00b0e3e30e 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -41,10 +41,18 @@ func TestQueryProjectionCompat(t *testing.T) { projection: bson.D{{"v", int32(0)}}, }, "DotNotationInclude": { + filter: bson.D{}, + projection: bson.D{{"v.foo", true}}, + }, + "DotNotationIncludeTwo": { filter: bson.D{}, projection: bson.D{{"v.foo", true}, {"v.array", true}}, }, "DotNotationExclude": { + filter: bson.D{}, + projection: bson.D{{"v.foo", false}}, + }, + "DotNotationExcludeTwo": { filter: bson.D{}, projection: bson.D{{"v.foo", false}, {"v.array", false}}, }, diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 57f17f5f51d7..a80b613fd27b 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -33,9 +33,11 @@ var ( // validateProjection check projection document. // Document fields could be either included or excluded but not both. // Exception is for the _id field that could be included or excluded. -func validateProjection(projection *types.Document) (bool, error) { +func validateProjection(projection *types.Document) (*types.Document, bool, error) { + validated := types.MakeDocument(0) + if projection == nil { - return false, errProjectionEmpty + return nil, false, errProjectionEmpty } var projectionVal *bool @@ -50,7 +52,7 @@ func validateProjection(projection *types.Document) (bool, error) { } if err != nil { - return false, lazyerrors.Error(err) + return nil, false, lazyerrors.Error(err) } if key == "_id" { // _id is a special case and can be included or excluded @@ -61,7 +63,7 @@ func validateProjection(projection *types.Document) (bool, error) { switch value := value.(type) { case *types.Document: - return false, commonerrors.NewCommandErrorMsg( + return nil, false, commonerrors.NewCommandErrorMsg( commonerrors.ErrNotImplemented, fmt.Sprintf("projection expression %s is not supported", types.FormatAnyValue(value)), ) @@ -76,11 +78,11 @@ func validateProjection(projection *types.Document) (bool, error) { result = true } default: - return false, lazyerrors.Errorf("unsupported operation %s %value (%T)", key, value, value) + return nil, false, lazyerrors.Errorf("unsupported operation %s %value (%T)", key, value, value) } // set the value with boolean result to omit type assertion when we will apply projection - projection.Set(key, result) + validated.Set(key, result) // if projectionVal is nil we are processing the first field if projectionVal == nil { @@ -90,13 +92,13 @@ func validateProjection(projection *types.Document) (bool, error) { if *projectionVal != result { if *projectionVal { - return false, commonerrors.NewCommandErrorMsgWithArgument( + return nil, false, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrProjectionExIn, fmt.Sprintf("Cannot do exclusion on field %s in inclusion projection", key), "projection", ) } else { - return false, commonerrors.NewCommandErrorMsgWithArgument( + return nil, false, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrProjectionInEx, fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", key), "projection", @@ -105,7 +107,7 @@ func validateProjection(projection *types.Document) (bool, error) { } } - return *projectionVal == true, nil + return validated, *projectionVal == true, nil } // projectDocument applies projection to the copy of the document. diff --git a/internal/handlers/common/projection_iterator.go b/internal/handlers/common/projection_iterator.go index 81c262fe9f11..cbc4099f4ca3 100644 --- a/internal/handlers/common/projection_iterator.go +++ b/internal/handlers/common/projection_iterator.go @@ -27,7 +27,7 @@ import ( // Close method closes the underlying iterator. // For that reason, there is no need to track both iterators. func ProjectionIterator(iter types.DocumentsIterator, projection *types.Document) (types.DocumentsIterator, error) { - inclusion, err := validateProjection(projection) + projectionValidated, inclusion, err := validateProjection(projection) if errors.Is(err, errProjectionEmpty) { return iter, nil } @@ -38,7 +38,7 @@ func ProjectionIterator(iter types.DocumentsIterator, projection *types.Document return &projectionIterator{ iter: iter, - projection: projection, + projection: projectionValidated, inclusion: inclusion, }, nil } From ee8a08cd41acb6066e470c7030aa4dad6bd9094f Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 13 Apr 2023 20:10:02 +0100 Subject: [PATCH 17/46] WIP --- internal/handlers/common/projection.go | 71 ++++++++++++++++++--- internal/handlers/common/projection_test.go | 48 ++++++++++++++ 2 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 internal/handlers/common/projection_test.go diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index a80b613fd27b..54d79497edcc 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -17,12 +17,12 @@ package common import ( "errors" "fmt" + "github.com/FerretDB/FerretDB/internal/util/must" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/iterator" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" - "github.com/FerretDB/FerretDB/internal/util/must" ) var ( @@ -113,6 +113,7 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro // projectDocument applies projection to the copy of the document. func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Document, error) { var projected *types.Document + if inclusion { projected = types.MakeDocument(1) @@ -149,21 +150,75 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do ) case bool: // field: bool - // if projection value is false, we should skip the field - if !value { - projected.RemoveByPath(path) + + // process top level fields + if path.Len() == 1 { + if inclusion { + if doc.Has(key) { + projected.Set(key, must.NotFail(doc.Get(key))) + } + } else { + projected.Remove(key) + } + continue } + // process nested fields + projected = walkProjectionPath(path, value, projected, doc) default: return nil, lazyerrors.Errorf("unsupported operation %s %v (%T)", key, value, value) } + } + + return projected, nil +} + +func walkProjectionPath(path types.Path, inclusion bool, projected, doc *types.Document) *types.Document { + next := types.NewStaticPath(path.Slice()...) - // if doc has field set it to the projected document - if doc.HasByPath(path) { - projected.SetByPath(path, must.NotFail(doc.GetByPath(path))) + for next.Len() > 1 { + _, err := doc.GetByPath(next) + if err == nil { + break } + + next = next.TrimSuffix() } - return projected, nil + if inclusion { + if path.Len() == next.Len() { + projected.SetByPath(path, must.NotFail(doc.GetByPath(next))) + + return projected + } + + value, err := doc.GetByPath(next) + if err != nil { + return projected + } + + switch value.(type) { + case *types.Document: + if next.Len() > 1 { + projected.SetByPath(next.TrimSuffix(), types.MakeDocument(0)) + } else { + projected.Set(next.Prefix(), types.MakeDocument(0)) + } + case *types.Array: + if next.Len() > 1 { + projected.SetByPath(next.TrimSuffix(), types.MakeArray(0)) + } else { + projected.Set(next.Prefix(), types.MakeArray(0)) + } + default: + if next.Len() > 1 { + projected.SetByPath(next.TrimSuffix(), types.MakeDocument(0)) + } + } + } else { + projected.RemoveByPath(next) + } + + return projected } diff --git a/internal/handlers/common/projection_test.go b/internal/handlers/common/projection_test.go new file mode 100644 index 000000000000..c3f56564a449 --- /dev/null +++ b/internal/handlers/common/projection_test.go @@ -0,0 +1,48 @@ +package common + +import ( + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_walkProjectionPath(t *testing.T) { + testCases := map[string]struct { + path types.Path + inclusion bool + projected *types.Document + doc *types.Document + expected *types.Document + }{ + "Inclusion2Level2": { + path: types.NewStaticPath("a", "b"), + inclusion: true, + projected: must.NotFail(types.NewDocument("_id", "level2")), + doc: must.NotFail(types.NewDocument("_id", "level2", "a", must.NotFail(types.NewDocument("b", 12)))), + expected: must.NotFail(types.NewDocument("_id", "level2", "a", must.NotFail(types.NewDocument("b", 12)))), + }, + "Inclusion3Level3": { + path: types.NewStaticPath("a", "b", "c"), + inclusion: true, + projected: must.NotFail(types.NewDocument("_id", "level3")), + doc: must.NotFail(types.NewDocument("_id", "level3", "a", must.NotFail(types.NewDocument("b", must.NotFail(types.NewDocument("c", 12)))))), + expected: must.NotFail(types.NewDocument("_id", "level3", "a", must.NotFail(types.NewDocument("b", must.NotFail(types.NewDocument("c", 12)))))), + }, + "Inclusion3Level2": { + path: types.NewStaticPath("a", "b", "c"), + inclusion: true, + projected: must.NotFail(types.NewDocument("_id", "level2")), + doc: must.NotFail(types.NewDocument("_id", "level2", "a", must.NotFail(types.NewDocument("b", 12)))), + expected: must.NotFail(types.NewDocument("_id", "level2", "a", types.MakeDocument(0))), + }, + } + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + res := walkProjectionPath(tc.path, tc.inclusion, tc.projected, tc.doc) + + assert.Equal(t, tc.expected, res) + }) + } +} From 7b806b0ff4dea3589dd31fefca00db4b1326f5a8 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 14 Apr 2023 12:56:10 +0100 Subject: [PATCH 18/46] WIP --- integration/query_projection_compat_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index 4b00b0e3e30e..250997790a64 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -43,27 +43,33 @@ func TestQueryProjectionCompat(t *testing.T) { "DotNotationInclude": { filter: bson.D{}, projection: bson.D{{"v.foo", true}}, + skip: "https://github.com/FerretDB/FerretDB/issues/2430", }, "DotNotationIncludeTwo": { filter: bson.D{}, projection: bson.D{{"v.foo", true}, {"v.array", true}}, + skip: "https://github.com/FerretDB/FerretDB/issues/2430", }, "DotNotationExclude": { filter: bson.D{}, projection: bson.D{{"v.foo", false}}, + skip: "https://github.com/FerretDB/FerretDB/issues/2430", }, "DotNotationExcludeTwo": { filter: bson.D{}, projection: bson.D{{"v.foo", false}, {"v.array", false}}, + skip: "https://github.com/FerretDB/FerretDB/issues/2430", }, "DotNotationExcludeSecondLevel": { filter: bson.D{}, projection: bson.D{{"v.array.42", false}}, + skip: "https://github.com/FerretDB/FerretDB/issues/2430", }, "DotNotationIncludeExclude": { filter: bson.D{}, projection: bson.D{{"v.foo", true}, {"v.array", false}}, resultType: emptyResult, + skip: "https://github.com/FerretDB/FerretDB/issues/2430", }, } From e318ce6d4648efa706fcb6b30b94117991e7aa20 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 14 Apr 2023 13:08:50 +0100 Subject: [PATCH 19/46] WIP --- integration/shareddata/scalars.go | 1 + integration/shareddata/shareddata.go | 14 +++-- internal/handlers/common/projection.go | 58 +------------------ .../handlers/common/projection_iterator.go | 1 + internal/handlers/common/projection_test.go | 48 --------------- 5 files changed, 15 insertions(+), 107 deletions(-) delete mode 100644 internal/handlers/common/projection_test.go diff --git a/integration/shareddata/scalars.go b/integration/shareddata/scalars.go index 77dbadd44697..5841c0e28c9d 100644 --- a/integration/shareddata/scalars.go +++ b/integration/shareddata/scalars.go @@ -434,6 +434,7 @@ var ObjectIDKeys = &Values[primitive.ObjectID]{ }, } +// TopLevelFieldsIntegers contains documents with several top level fields with integer values. var TopLevelFieldsIntegers = &TopLevelValues[string]{ name: "TopLevelFieldsIntegers", backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, diff --git a/integration/shareddata/shareddata.go b/integration/shareddata/shareddata.go index 3bc12f781b5f..ab1a5c6d4176 100644 --- a/integration/shareddata/shareddata.go +++ b/integration/shareddata/shareddata.go @@ -203,6 +203,8 @@ func (values *Values[idType]) IsCompatible(backend string) bool { type fields map[string]any // TopLevelValues stores shared data documents as {"_id": key, "field1": value1, "field2": value2, ...} documents. +// +//nolint:vet // for readability type TopLevelValues[id comparable] struct { name string backends []string @@ -220,9 +222,11 @@ func (t *TopLevelValues[id]) Validators(backend, collection string) map[string]a switch backend { case "ferretdb-tigris": validators := make(map[string]any, len(t.validators[backend])) + for key, value := range t.validators[backend] { validators[key] = strings.ReplaceAll(value.(string), "%%collection%%", collection) } + return validators default: return t.validators[backend] @@ -234,14 +238,16 @@ func (t *TopLevelValues[id]) Docs() []bson.D { ids := maps.Keys(t.data) res := make([]bson.D, 0, len(t.data)) + for _, id := range ids { doc := bson.D{{"_id", id}} + fields := t.data[id] - if fields != nil { - for key, field := range fields { - doc = append(doc, bson.E{Key: key, Value: field}) - } + + for key, field := range fields { + doc = append(doc, bson.E{Key: key, Value: field}) } + res = append(res, doc) } diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 54d79497edcc..79ac95c7fab5 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -17,16 +17,15 @@ package common import ( "errors" "fmt" - "github.com/FerretDB/FerretDB/internal/util/must" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/iterator" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" ) var ( - // errProjectionEmpty is returned when projection document is empty. errProjectionEmpty = errors.New("projection is empty") ) @@ -107,7 +106,7 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro } } - return validated, *projectionVal == true, nil + return validated, *projectionVal, nil } // projectDocument applies projection to the copy of the document. @@ -150,7 +149,6 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do ) case bool: // field: bool - // process top level fields if path.Len() == 1 { if inclusion { @@ -164,8 +162,7 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do continue } - // process nested fields - projected = walkProjectionPath(path, value, projected, doc) + // TODO: process dot notation here https://github.com/FerretDB/FerretDB/issues/2430 default: return nil, lazyerrors.Errorf("unsupported operation %s %v (%T)", key, value, value) } @@ -173,52 +170,3 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do return projected, nil } - -func walkProjectionPath(path types.Path, inclusion bool, projected, doc *types.Document) *types.Document { - next := types.NewStaticPath(path.Slice()...) - - for next.Len() > 1 { - _, err := doc.GetByPath(next) - if err == nil { - break - } - - next = next.TrimSuffix() - } - - if inclusion { - if path.Len() == next.Len() { - projected.SetByPath(path, must.NotFail(doc.GetByPath(next))) - - return projected - } - - value, err := doc.GetByPath(next) - if err != nil { - return projected - } - - switch value.(type) { - case *types.Document: - if next.Len() > 1 { - projected.SetByPath(next.TrimSuffix(), types.MakeDocument(0)) - } else { - projected.Set(next.Prefix(), types.MakeDocument(0)) - } - case *types.Array: - if next.Len() > 1 { - projected.SetByPath(next.TrimSuffix(), types.MakeArray(0)) - } else { - projected.Set(next.Prefix(), types.MakeArray(0)) - } - default: - if next.Len() > 1 { - projected.SetByPath(next.TrimSuffix(), types.MakeDocument(0)) - } - } - } else { - projected.RemoveByPath(next) - } - - return projected -} diff --git a/internal/handlers/common/projection_iterator.go b/internal/handlers/common/projection_iterator.go index cbc4099f4ca3..39cd459ee0ac 100644 --- a/internal/handlers/common/projection_iterator.go +++ b/internal/handlers/common/projection_iterator.go @@ -16,6 +16,7 @@ package common import ( "errors" + "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" ) diff --git a/internal/handlers/common/projection_test.go b/internal/handlers/common/projection_test.go deleted file mode 100644 index c3f56564a449..000000000000 --- a/internal/handlers/common/projection_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package common - -import ( - "github.com/FerretDB/FerretDB/internal/types" - "github.com/FerretDB/FerretDB/internal/util/must" - "github.com/stretchr/testify/assert" - "testing" -) - -func Test_walkProjectionPath(t *testing.T) { - testCases := map[string]struct { - path types.Path - inclusion bool - projected *types.Document - doc *types.Document - expected *types.Document - }{ - "Inclusion2Level2": { - path: types.NewStaticPath("a", "b"), - inclusion: true, - projected: must.NotFail(types.NewDocument("_id", "level2")), - doc: must.NotFail(types.NewDocument("_id", "level2", "a", must.NotFail(types.NewDocument("b", 12)))), - expected: must.NotFail(types.NewDocument("_id", "level2", "a", must.NotFail(types.NewDocument("b", 12)))), - }, - "Inclusion3Level3": { - path: types.NewStaticPath("a", "b", "c"), - inclusion: true, - projected: must.NotFail(types.NewDocument("_id", "level3")), - doc: must.NotFail(types.NewDocument("_id", "level3", "a", must.NotFail(types.NewDocument("b", must.NotFail(types.NewDocument("c", 12)))))), - expected: must.NotFail(types.NewDocument("_id", "level3", "a", must.NotFail(types.NewDocument("b", must.NotFail(types.NewDocument("c", 12)))))), - }, - "Inclusion3Level2": { - path: types.NewStaticPath("a", "b", "c"), - inclusion: true, - projected: must.NotFail(types.NewDocument("_id", "level2")), - doc: must.NotFail(types.NewDocument("_id", "level2", "a", must.NotFail(types.NewDocument("b", 12)))), - expected: must.NotFail(types.NewDocument("_id", "level2", "a", types.MakeDocument(0))), - }, - } - for name, tc := range testCases { - name, tc := name, tc - t.Run(name, func(t *testing.T) { - res := walkProjectionPath(tc.path, tc.inclusion, tc.projected, tc.doc) - - assert.Equal(t, tc.expected, res) - }) - } -} From 683b60bf1fff4fee412b86be5e5fbf3cd1db8edc Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 14 Apr 2023 13:23:57 +0100 Subject: [PATCH 20/46] WIP --- integration/query_compat_test.go | 8 ++++++-- integration/query_projection_compat_test.go | 8 ++++++++ internal/handlers/common/projection.go | 4 ---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/integration/query_compat_test.go b/integration/query_compat_test.go index 1136aab43191..6aba9d1f1b0d 100644 --- a/integration/query_compat_test.go +++ b/integration/query_compat_test.go @@ -41,6 +41,7 @@ type queryCompatTestCase struct { resultType compatTestCaseResultType // defaults to nonEmptyResult resultPushdown bool // defaults to false + skipIDCheck bool // skip check collected IDs, check results only skip string // skip test for all handlers, must have issue number mentioned skipForTigris string // skip test for Tigris } @@ -151,8 +152,11 @@ func testQueryCompat(t *testing.T, testCases map[string]queryCompatTestCase) { require.NoError(t, targetCursor.All(ctx, &targetRes)) require.NoError(t, compatCursor.All(ctx, &compatRes)) - t.Logf("Compat (expected) IDs: %v", CollectIDs(t, compatRes)) - t.Logf("Target (actual) IDs: %v", CollectIDs(t, targetRes)) + if tc.skipIDCheck { + t.Logf("Compat (expected) IDs: %v", CollectIDs(t, compatRes)) + t.Logf("Target (actual) IDs: %v", CollectIDs(t, targetRes)) + } + AssertEqualDocumentsSlice(t, compatRes, targetRes) if len(targetRes) > 0 || len(compatRes) > 0 { diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index 250997790a64..8d4ef8a85f73 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -36,6 +36,14 @@ func TestQueryProjectionCompat(t *testing.T) { filter: bson.D{}, projection: bson.D{{"v", int32(1)}}, }, + "IncludeID": { + filter: bson.D{}, + projection: bson.D{{"_id", true}}, + }, + "ExcludeID": { + filter: bson.D{}, + projection: bson.D{{"_id", false}}, + }, "ExcludeField": { filter: bson.D{}, projection: bson.D{{"v", int32(0)}}, diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 79ac95c7fab5..e877eea403b9 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -54,10 +54,6 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro return nil, false, lazyerrors.Error(err) } - if key == "_id" { // _id is a special case and can be included or excluded - continue - } - var result bool switch value := value.(type) { From 3901421ffcd99fc731fcd96f0682c463a49d08c6 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 14 Apr 2023 13:39:08 +0100 Subject: [PATCH 21/46] WIP --- internal/handlers/common/projection.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index e877eea403b9..95fca21b59c2 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -25,9 +25,7 @@ import ( "github.com/FerretDB/FerretDB/internal/util/must" ) -var ( - errProjectionEmpty = errors.New("projection is empty") -) +var errProjectionEmpty = errors.New("projection is empty") // validateProjection check projection document. // Document fields could be either included or excluded but not both. From b127a3cc9b47f61642c601cb5404c8947908f217 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 14 Apr 2023 18:20:37 +0100 Subject: [PATCH 22/46] WIP --- integration/query_projection_compat_test.go | 38 +++++------- internal/handlers/common/projection.go | 68 +++++++++++++++++++-- 2 files changed, 78 insertions(+), 28 deletions(-) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index 8d4ef8a85f73..db2bfd95521b 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -24,17 +24,21 @@ func TestQueryProjectionCompat(t *testing.T) { t.Parallel() testCases := map[string]queryCompatTestCase{ - "FindProjectionInclusions": { + "Include1Field": { filter: bson.D{}, - projection: bson.D{{"foo", int32(1)}, {"bar", true}}, + projection: bson.D{{"v", int32(1)}}, }, - "FindProjectionExclusions": { + "Exclude1Field": { filter: bson.D{}, - projection: bson.D{{"foo", int32(0)}, {"bar", false}}, + projection: bson.D{{"v", int32(0)}}, }, - "IncludeField": { + "Include2Fields": { filter: bson.D{}, - projection: bson.D{{"v", int32(1)}}, + projection: bson.D{{"foo", int32(1)}, {"bar", true}}, + }, + "Exclude2Fields": { + filter: bson.D{}, + projection: bson.D{{"foo", int32(0)}, {"bar", false}}, }, "IncludeID": { filter: bson.D{}, @@ -44,9 +48,13 @@ func TestQueryProjectionCompat(t *testing.T) { filter: bson.D{}, projection: bson.D{{"_id", false}}, }, - "ExcludeField": { + "IncludeFieldExcludeID": { filter: bson.D{}, - projection: bson.D{{"v", int32(0)}}, + projection: bson.D{{"_id", false}, {"array", true}}, + }, + "ExcludeFieldIncludeID": { + filter: bson.D{}, + projection: bson.D{{"_id", true}, {"array", false}}, }, "DotNotationInclude": { filter: bson.D{}, @@ -83,17 +91,3 @@ func TestQueryProjectionCompat(t *testing.T) { testQueryCompat(t, testCases) } - -func TestQueryProjectionCompatCommand(t *testing.T) { - t.Parallel() - - testCases := map[string]queryCompatCommandTestCase{ - "FindProjectionIDExclusion": { - filter: bson.D{{"_id", "document-composite"}}, - projection: bson.D{{"_id", false}, {"array", int32(1)}}, - resultPushdown: true, - }, - } - - testQueryCompatCommand(t, testCases) -} diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 95fca21b59c2..c6e86ca2c058 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -18,6 +18,8 @@ import ( "errors" "fmt" + "github.com/AlekSi/pointer" + "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/iterator" @@ -67,9 +69,7 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro result = true } case bool: - if value { - result = true - } + result = value default: return nil, false, lazyerrors.Errorf("unsupported operation %s %value (%T)", key, value, value) } @@ -79,6 +79,10 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro // if projectionVal is nil we are processing the first field if projectionVal == nil { + if key == "_id" { + continue + } + projectionVal = &result continue } @@ -100,17 +104,69 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro } } + if projection.Len() == 1 && projection.Has("_id") { + projectionVal = pointer.ToBool(false) + } + return validated, *projectionVal, nil } // projectDocument applies projection to the copy of the document. func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Document, error) { + projected := types.MakeDocument(1) + + if projection.Has("_id") { + idValue := must.NotFail(projection.Get("_id")) + + var set bool + + switch idValue := idValue.(type) { + case *types.Document: // field: { $elemMatch: { field2: value }} + return nil, commonerrors.NewCommandErrorMsg( + commonerrors.ErrCommandNotFound, + fmt.Sprintf("projection %s is not supported", + types.FormatAnyValue(idValue), + ), + ) + case bool: + set = idValue + default: + return nil, lazyerrors.Errorf("unsupported operation %s %v (%T)", "_id", idValue, idValue) + } + + if set { + projected.Set("_id", must.NotFail(doc.Get("_id"))) + } + } else { + if inclusion { + projected.Set("_id", must.NotFail(doc.Get("_id"))) + } + } + + projectionWithoutID := projection.DeepCopy() + + projectionWithoutID.Remove("_id") + + docWithoutID := doc.DeepCopy() + docWithoutID.Remove("_id") + + projectedWithoutID, err := projectDoc(docWithoutID, projectionWithoutID, inclusion) + if err != nil { + return nil, err + } + + for _, key := range projectedWithoutID.Keys() { + projected.Set(key, must.NotFail(projectedWithoutID.Get(key))) + } + + return projected, nil +} + +func projectDoc(doc *types.Document, projection *types.Document, inclusion bool) (*types.Document, error) { var projected *types.Document if inclusion { - projected = types.MakeDocument(1) - - projected.Set("_id", must.NotFail(doc.Get("_id"))) + projected = types.MakeDocument(0) } else { projected = doc.DeepCopy() } From d929cad465d8861f5a5cc77bdb8e8078d68281ec Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sun, 16 Apr 2023 13:24:41 +0100 Subject: [PATCH 23/46] WIP --- internal/handlers/common/projection.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index c6e86ca2c058..21bc088bf968 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -137,10 +137,6 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do if set { projected.Set("_id", must.NotFail(doc.Get("_id"))) } - } else { - if inclusion { - projected.Set("_id", must.NotFail(doc.Get("_id"))) - } } projectionWithoutID := projection.DeepCopy() @@ -150,6 +146,10 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do docWithoutID := doc.DeepCopy() docWithoutID.Remove("_id") + if projection.Len() == 1 && projection.Has("_id") { + return projected, nil + } + projectedWithoutID, err := projectDoc(docWithoutID, projectionWithoutID, inclusion) if err != nil { return nil, err @@ -163,12 +163,10 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do } func projectDoc(doc *types.Document, projection *types.Document, inclusion bool) (*types.Document, error) { - var projected *types.Document + projected := doc.DeepCopy() if inclusion { projected = types.MakeDocument(0) - } else { - projected = doc.DeepCopy() } iter := projection.Iterator() @@ -205,11 +203,11 @@ func projectDoc(doc *types.Document, projection *types.Document, inclusion bool) if doc.Has(key) { projected.Set(key, must.NotFail(doc.Get(key))) } - } else { - projected.Remove(key) + + continue } - continue + projected.Remove(key) } // TODO: process dot notation here https://github.com/FerretDB/FerretDB/issues/2430 From b6c79f2d992970e47f78867ef6badaad35a76ef0 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 17 Apr 2023 11:49:20 +0100 Subject: [PATCH 24/46] WIP --- integration/query_projection_compat_test.go | 4 ++-- internal/handlers/common/projection.go | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index db2bfd95521b..b0bdc143ff9d 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -50,11 +50,11 @@ func TestQueryProjectionCompat(t *testing.T) { }, "IncludeFieldExcludeID": { filter: bson.D{}, - projection: bson.D{{"_id", false}, {"array", true}}, + projection: bson.D{{"_id", false}, {"v", true}}, }, "ExcludeFieldIncludeID": { filter: bson.D{}, - projection: bson.D{{"_id", true}, {"array", false}}, + projection: bson.D{{"_id", true}, {"v", false}}, }, "DotNotationInclude": { filter: bson.D{}, diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 21bc088bf968..b622c1c636d4 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -113,7 +113,10 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro // projectDocument applies projection to the copy of the document. func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Document, error) { - projected := types.MakeDocument(1) + projected, err := types.NewDocument("_id", must.NotFail(doc.Get("_id"))) + if err != nil { + return nil, err + } if projection.Has("_id") { idValue := must.NotFail(projection.Get("_id")) @@ -134,8 +137,8 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do return nil, lazyerrors.Errorf("unsupported operation %s %v (%T)", "_id", idValue, idValue) } - if set { - projected.Set("_id", must.NotFail(doc.Get("_id"))) + if !set { + projected.Remove("_id") } } @@ -163,10 +166,10 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do } func projectDoc(doc *types.Document, projection *types.Document, inclusion bool) (*types.Document, error) { - projected := doc.DeepCopy() + projected := types.MakeDocument(0) if inclusion { - projected = types.MakeDocument(0) + projected = doc.DeepCopy() } iter := projection.Iterator() From 00d31e0a3bbfd57a4a2da344d1e5c16d16924845 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 17 Apr 2023 11:54:11 +0100 Subject: [PATCH 25/46] WIP --- internal/handlers/common/projection_iterator.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/handlers/common/projection_iterator.go b/internal/handlers/common/projection_iterator.go index 822ec8ba2e37..c19a4b98d256 100644 --- a/internal/handlers/common/projection_iterator.go +++ b/internal/handlers/common/projection_iterator.go @@ -34,6 +34,10 @@ func ProjectionIterator(iter types.DocumentsIterator, closer *iterator.MultiClos return iter, nil } + if err != nil { + return nil, lazyerrors.Error(err) + } + res := &projectionIterator{ iter: iter, projection: projectionValidated, From 08a1b7822f9ce98cd2f86386d329c6a27735b8f7 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 17 Apr 2023 12:39:19 +0100 Subject: [PATCH 26/46] WIP --- internal/handlers/common/projection.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index b622c1c636d4..707d4af52d09 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -105,7 +105,7 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro } if projection.Len() == 1 && projection.Has("_id") { - projectionVal = pointer.ToBool(false) + projectionVal = pointer.ToBool(must.NotFail(projection.Get("_id")).(bool)) } return validated, *projectionVal, nil @@ -143,16 +143,11 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do } projectionWithoutID := projection.DeepCopy() - projectionWithoutID.Remove("_id") docWithoutID := doc.DeepCopy() docWithoutID.Remove("_id") - if projection.Len() == 1 && projection.Has("_id") { - return projected, nil - } - projectedWithoutID, err := projectDoc(docWithoutID, projectionWithoutID, inclusion) if err != nil { return nil, err @@ -168,7 +163,7 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do func projectDoc(doc *types.Document, projection *types.Document, inclusion bool) (*types.Document, error) { projected := types.MakeDocument(0) - if inclusion { + if !inclusion { projected = doc.DeepCopy() } From 6c5be4f9307c44af986f4e5158d0d1c233d2c01e Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 17 Apr 2023 12:42:16 +0100 Subject: [PATCH 27/46] WIP --- integration/query_projection_compat_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index b0bdc143ff9d..228a69377d2e 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -56,6 +56,14 @@ func TestQueryProjectionCompat(t *testing.T) { filter: bson.D{}, projection: bson.D{{"_id", true}, {"v", false}}, }, + "ExcludeFieldExcludeID": { + filter: bson.D{}, + projection: bson.D{{"_id", false}, {"v", false}}, + }, + "IncludeFieldIncludeID": { + filter: bson.D{}, + projection: bson.D{{"_id", true}, {"v", true}}, + }, "DotNotationInclude": { filter: bson.D{}, projection: bson.D{{"v.foo", true}}, From c809be569ff01737226813ce3f98c7f3d26b080c Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 17 Apr 2023 12:48:32 +0100 Subject: [PATCH 28/46] WIP --- internal/handlers/common/projection.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 707d4af52d09..dae65f96965a 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -57,12 +57,11 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro var result bool switch value := value.(type) { - case *types.Document: + case *types.Document, *types.Array: return nil, false, commonerrors.NewCommandErrorMsg( commonerrors.ErrNotImplemented, fmt.Sprintf("projection expression %s is not supported", types.FormatAnyValue(value)), ) - case float64, int32, int64: comparison := types.Compare(value, int32(0)) if comparison != types.Equal { From b38ade9d35a5e94a20adcce5592642af7e080b05 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 17 Apr 2023 12:54:13 +0100 Subject: [PATCH 29/46] WIP --- integration/query_projection_compat_test.go | 4 ++-- internal/handlers/common/projection.go | 14 ++++---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index 228a69377d2e..46605a30d262 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -34,7 +34,7 @@ func TestQueryProjectionCompat(t *testing.T) { }, "Include2Fields": { filter: bson.D{}, - projection: bson.D{{"foo", int32(1)}, {"bar", true}}, + projection: bson.D{{"foo", 1.24}, {"bar", true}}, }, "Exclude2Fields": { filter: bson.D{}, @@ -42,7 +42,7 @@ func TestQueryProjectionCompat(t *testing.T) { }, "IncludeID": { filter: bson.D{}, - projection: bson.D{{"_id", true}}, + projection: bson.D{{"_id", int64(-1)}}, }, "ExcludeID": { filter: bson.D{}, diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index dae65f96965a..0fbc6578afc9 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -18,8 +18,6 @@ import ( "errors" "fmt" - "github.com/AlekSi/pointer" - "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/iterator" @@ -57,7 +55,7 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro var result bool switch value := value.(type) { - case *types.Document, *types.Array: + case *types.Document, *types.Array, string: return nil, false, commonerrors.NewCommandErrorMsg( commonerrors.ErrNotImplemented, fmt.Sprintf("projection expression %s is not supported", types.FormatAnyValue(value)), @@ -78,9 +76,9 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro // if projectionVal is nil we are processing the first field if projectionVal == nil { - if key == "_id" { - continue - } + //if key == "_id" { + // continue + //} projectionVal = &result continue @@ -103,10 +101,6 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro } } - if projection.Len() == 1 && projection.Has("_id") { - projectionVal = pointer.ToBool(must.NotFail(projection.Get("_id")).(bool)) - } - return validated, *projectionVal, nil } From 8d371eaece38639dd34470453f1b4be735a3d7cc Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 17 Apr 2023 13:00:39 +0100 Subject: [PATCH 30/46] WIP --- internal/handlers/common/projection.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 0fbc6578afc9..e0d8f9c111f4 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -76,11 +76,8 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro // if projectionVal is nil we are processing the first field if projectionVal == nil { - //if key == "_id" { - // continue - //} - projectionVal = &result + continue } From 43240eb5b91a2e3a2c512f5a05ac2c168533ccf0 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 17 Apr 2023 13:51:51 +0100 Subject: [PATCH 31/46] WIP --- integration/query_projection_compat_test.go | 4 ++++ internal/handlers/common/projection.go | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index 46605a30d262..c71873847387 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -40,6 +40,10 @@ func TestQueryProjectionCompat(t *testing.T) { filter: bson.D{}, projection: bson.D{{"foo", int32(0)}, {"bar", false}}, }, + "Include1FieldExclude1Field": { + filter: bson.D{}, + projection: bson.D{{"foo", int32(1)}, {"bar", true}}, + }, "IncludeID": { filter: bson.D{}, projection: bson.D{{"_id", int64(-1)}}, diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index e0d8f9c111f4..8d3369b58c60 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -88,13 +88,13 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro fmt.Sprintf("Cannot do exclusion on field %s in inclusion projection", key), "projection", ) - } else { - return nil, false, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrProjectionInEx, - fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", key), - "projection", - ) } + + return nil, false, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrProjectionInEx, + fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", key), + "projection", + ) } } From d8ff0d8c131933248abfc51b7240cd3cc12b5c23 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 17 Apr 2023 14:07:39 +0100 Subject: [PATCH 32/46] WIP --- internal/handlers/common/projection.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 8d3369b58c60..c7bbdc1c359a 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -74,8 +74,16 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro // set the value with boolean result to omit type assertion when we will apply projection validated.Set(key, result) + if projection.Len() == 1 && key == "_id" { + return validated, result, nil + } + // if projectionVal is nil we are processing the first field if projectionVal == nil { + if key == "_id" { + continue + } + projectionVal = &result continue From dfad59427ed213b82ec59e132f7cf7165ca2a15f Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 17 Apr 2023 14:10:09 +0100 Subject: [PATCH 33/46] WIP --- integration/query_compat_test.go | 2 +- integration/query_projection_compat_test.go | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/integration/query_compat_test.go b/integration/query_compat_test.go index 6aba9d1f1b0d..b8b3f1459af9 100644 --- a/integration/query_compat_test.go +++ b/integration/query_compat_test.go @@ -152,7 +152,7 @@ func testQueryCompat(t *testing.T, testCases map[string]queryCompatTestCase) { require.NoError(t, targetCursor.All(ctx, &targetRes)) require.NoError(t, compatCursor.All(ctx, &compatRes)) - if tc.skipIDCheck { + if !tc.skipIDCheck { t.Logf("Compat (expected) IDs: %v", CollectIDs(t, compatRes)) t.Logf("Target (actual) IDs: %v", CollectIDs(t, targetRes)) } diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index c71873847387..217eef9e380a 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -49,20 +49,23 @@ func TestQueryProjectionCompat(t *testing.T) { projection: bson.D{{"_id", int64(-1)}}, }, "ExcludeID": { - filter: bson.D{}, - projection: bson.D{{"_id", false}}, + filter: bson.D{}, + projection: bson.D{{"_id", false}}, + skipIDCheck: true, }, "IncludeFieldExcludeID": { - filter: bson.D{}, - projection: bson.D{{"_id", false}, {"v", true}}, + filter: bson.D{}, + projection: bson.D{{"_id", false}, {"v", true}}, + skipIDCheck: true, }, "ExcludeFieldIncludeID": { filter: bson.D{}, projection: bson.D{{"_id", true}, {"v", false}}, }, "ExcludeFieldExcludeID": { - filter: bson.D{}, - projection: bson.D{{"_id", false}, {"v", false}}, + filter: bson.D{}, + projection: bson.D{{"_id", false}, {"v", false}}, + skipIDCheck: true, }, "IncludeFieldIncludeID": { filter: bson.D{}, From 70315e5fb3507aeeca78ed6425fd01b0d544703c Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 17 Apr 2023 15:28:13 +0100 Subject: [PATCH 34/46] WIP --- Taskfile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Taskfile.yml b/Taskfile.yml index 7c56d9395e82..7682ca34b076 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -5,7 +5,7 @@ env: GORACE: halt_on_error=1,history_size=2 vars: - INTEGRATIONTIME: 30m + INTEGRATIONTIME: 35m BENCHTIME: 5s FUZZTIME: 15s FUZZCORPUS: ../fuzz-corpus From 4a53babf9f1c89359e6a0cee4f50c087a85b7cca Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 17 Apr 2023 18:57:33 +0100 Subject: [PATCH 35/46] WIP --- integration/query_compat_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/query_compat_test.go b/integration/query_compat_test.go index b8b3f1459af9..f06c37f97157 100644 --- a/integration/query_compat_test.go +++ b/integration/query_compat_test.go @@ -41,7 +41,7 @@ type queryCompatTestCase struct { resultType compatTestCaseResultType // defaults to nonEmptyResult resultPushdown bool // defaults to false - skipIDCheck bool // skip check collected IDs, check results only + skipIDCheck bool // skip check collected IDs, use it when no ids returned from query skip string // skip test for all handlers, must have issue number mentioned skipForTigris string // skip test for Tigris } From 8948faf92b38a349038a7db44a16198011d7cc1d Mon Sep 17 00:00:00 2001 From: Dmitry Date: Mon, 17 Apr 2023 19:00:41 +0100 Subject: [PATCH 36/46] WIP --- integration/shareddata/composites.go | 25 +++++++++++++++++++++++++ integration/shareddata/scalars.go | 25 ------------------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/integration/shareddata/composites.go b/integration/shareddata/composites.go index 5f71f46d1336..99ee9011e1ca 100644 --- a/integration/shareddata/composites.go +++ b/integration/shareddata/composites.go @@ -382,3 +382,28 @@ var ArrayDocuments = &Values[string]{ }, }, } + +// TopLevelFieldsIntegers contains documents with several top level fields with integer values. +var TopLevelFieldsIntegers = &TopLevelValues[string]{ + name: "TopLevelFieldsIntegers", + backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + validators: map[string]map[string]any{ + "ferretdb-tigris": { + "$tigrisSchemaString": `{ + "title": "%%collection%%", + "primary_key": ["_id"], + "properties": { + "foo": {"type": "integer", "format": "int32"}, + "bar": {"type": "integer", "format": "int32"}, + "_id": {"type": "string"} + } + }`, + }, + }, + data: map[string]fields{ + "int32-two": { + "foo": int32(1), + "bar": int32(2), + }, + }, +} diff --git a/integration/shareddata/scalars.go b/integration/shareddata/scalars.go index 5841c0e28c9d..8c639a383a38 100644 --- a/integration/shareddata/scalars.go +++ b/integration/shareddata/scalars.go @@ -434,31 +434,6 @@ var ObjectIDKeys = &Values[primitive.ObjectID]{ }, } -// TopLevelFieldsIntegers contains documents with several top level fields with integer values. -var TopLevelFieldsIntegers = &TopLevelValues[string]{ - name: "TopLevelFieldsIntegers", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, - validators: map[string]map[string]any{ - "ferretdb-tigris": { - "$tigrisSchemaString": `{ - "title": "%%collection%%", - "primary_key": ["_id"], - "properties": { - "foo": {"type": "integer", "format": "int32"}, - "bar": {"type": "integer", "format": "int32"}, - "_id": {"type": "string"} - } - }`, - }, - }, - data: map[string]fields{ - "int32-two": { - "foo": int32(1), - "bar": int32(2), - }, - }, -} - // tigrisSchema returns a tigris schema for the given type. // In addition to the "v" field, it also sets "foo" field with the same type, so it can be used // for test cases where multiple fields need to be supported (for example, $rename). From 3eb6df75db773a44947fd48a01079f1e3f6fd6a5 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 18 Apr 2023 10:30:31 +0100 Subject: [PATCH 37/46] WIP --- integration/query_compat_test.go | 18 ++++++++++-- integration/query_projection_compat_test.go | 31 ++++++++++++++++++++- integration/shareddata/composites.go | 25 ----------------- integration/shareddata/shareddata.go | 26 ++++++++++++----- 4 files changed, 64 insertions(+), 36 deletions(-) diff --git a/integration/query_compat_test.go b/integration/query_compat_test.go index f06c37f97157..939ce9afc297 100644 --- a/integration/query_compat_test.go +++ b/integration/query_compat_test.go @@ -46,13 +46,18 @@ type queryCompatTestCase struct { skipForTigris string // skip test for Tigris } -// testQueryCompat tests query compatibility test cases. -func testQueryCompat(t *testing.T, testCases map[string]queryCompatTestCase) { +func testQueryCompatWithProviders(t *testing.T, providers shareddata.Providers, testCases map[string]queryCompatTestCase) { t.Helper() + require.NotEmpty(t, providers) + // Use shared setup because find queries can't modify data. // TODO Use read-only user. https://github.com/FerretDB/FerretDB/issues/1025 - ctx, targetCollections, compatCollections := setup.SetupCompat(t) + s := setup.SetupCompatWithOpts(t, &setup.SetupCompatOpts{ + Providers: providers, + }) + + ctx, targetCollections, compatCollections := s.Ctx, s.TargetCollections, s.CompatCollections for name, tc := range testCases { name, tc := name, tc @@ -177,6 +182,13 @@ func testQueryCompat(t *testing.T, testCases map[string]queryCompatTestCase) { } } +// testQueryCompat tests query compatibility test cases. +func testQueryCompat(t *testing.T, testCases map[string]queryCompatTestCase) { + t.Helper() + + testQueryCompatWithProviders(t, shareddata.AllProviders(), testCases) +} + func TestQueryCompatFilter(t *testing.T) { t.Parallel() diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index 217eef9e380a..668ae0f5dd5d 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -18,11 +18,40 @@ import ( "testing" "go.mongodb.org/mongo-driver/bson" + + "github.com/FerretDB/FerretDB/integration/shareddata" ) func TestQueryProjectionCompat(t *testing.T) { t.Parallel() + // topLevelFieldsIntegers contains documents with several top level fields with integer values. + var topLevelFieldsIntegers = shareddata.NewTopLevelFieldsProvider[string]( + "TopLevelFieldsIntegers", + []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + map[string]map[string]any{ + "ferretdb-tigris": { + "$tigrisSchemaString": `{ + "title": "%%collection%%", + "primary_key": ["_id"], + "properties": { + "foo": {"type": "integer", "format": "int32"}, + "bar": {"type": "integer", "format": "int32"}, + "_id": {"type": "string"} + } + }`, + }, + }, + map[string]shareddata.Fields{ + "int32-two": { + {"foo", int32(1)}, + {"bar", int32(2)}, + }, + }, + ) + + providers := append(shareddata.AllProviders(), topLevelFieldsIntegers) + testCases := map[string]queryCompatTestCase{ "Include1Field": { filter: bson.D{}, @@ -104,5 +133,5 @@ func TestQueryProjectionCompat(t *testing.T) { }, } - testQueryCompat(t, testCases) + testQueryCompatWithProviders(t, providers, testCases) } diff --git a/integration/shareddata/composites.go b/integration/shareddata/composites.go index 99ee9011e1ca..5f71f46d1336 100644 --- a/integration/shareddata/composites.go +++ b/integration/shareddata/composites.go @@ -382,28 +382,3 @@ var ArrayDocuments = &Values[string]{ }, }, } - -// TopLevelFieldsIntegers contains documents with several top level fields with integer values. -var TopLevelFieldsIntegers = &TopLevelValues[string]{ - name: "TopLevelFieldsIntegers", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, - validators: map[string]map[string]any{ - "ferretdb-tigris": { - "$tigrisSchemaString": `{ - "title": "%%collection%%", - "primary_key": ["_id"], - "properties": { - "foo": {"type": "integer", "format": "int32"}, - "bar": {"type": "integer", "format": "int32"}, - "_id": {"type": "string"} - } - }`, - }, - }, - data: map[string]fields{ - "int32-two": { - "foo": int32(1), - "bar": int32(2), - }, - }, -} diff --git a/integration/shareddata/shareddata.go b/integration/shareddata/shareddata.go index ab1a5c6d4176..482ecf88bf6e 100644 --- a/integration/shareddata/shareddata.go +++ b/integration/shareddata/shareddata.go @@ -81,8 +81,6 @@ func AllProviders() Providers { Mixed, // TODO https://github.com/FerretDB/FerretDB/issues/2291 // ArrayAndDocuments, - - TopLevelFieldsIntegers, } // check that names are unique and randomize order @@ -199,8 +197,22 @@ func (values *Values[idType]) IsCompatible(backend string) bool { return slices.Contains(values.backends, backend) } -// fields is a map of field name -> value. -type fields map[string]any +type field struct { + Key string + Value any +} + +// Fields is a map of field name -> value. +type Fields []field + +func NewTopLevelFieldsProvider[id comparable](name string, backends []string, validators map[string]map[string]any, data map[id]Fields) *TopLevelValues[id] { + return &TopLevelValues[id]{ + name: name, + backends: backends, + validators: validators, + data: data, + } +} // TopLevelValues stores shared data documents as {"_id": key, "field1": value1, "field2": value2, ...} documents. // @@ -209,7 +221,7 @@ type TopLevelValues[id comparable] struct { name string backends []string validators map[string]map[string]any // backend -> validator name -> validator - data map[id]fields + data map[id]Fields } // Name implements Provider interface. @@ -244,8 +256,8 @@ func (t *TopLevelValues[id]) Docs() []bson.D { fields := t.data[id] - for key, field := range fields { - doc = append(doc, bson.E{Key: key, Value: field}) + for _, field := range fields { + doc = append(doc, bson.E{Key: field.Key, Value: field.Value}) } res = append(res, doc) From 4111fa286151e1f64bea65db4d4e8673f97e4351 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 18 Apr 2023 10:33:54 +0100 Subject: [PATCH 38/46] WIP --- internal/handlers/common/projection.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index c7bbdc1c359a..4e829d8cb677 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -61,7 +61,9 @@ func validateProjection(projection *types.Document) (*types.Document, bool, erro fmt.Sprintf("projection expression %s is not supported", types.FormatAnyValue(value)), ) case float64, int32, int64: + // projection treats 0 as false and any other value as true comparison := types.Compare(value, int32(0)) + if comparison != types.Equal { result = true } From 9460b91df1b077afc3c3c894e76ee98513212b21 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 18 Apr 2023 10:51:10 +0100 Subject: [PATCH 39/46] WIP --- integration/query_projection_compat_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index 668ae0f5dd5d..e0022c67cd9b 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -26,7 +26,7 @@ func TestQueryProjectionCompat(t *testing.T) { t.Parallel() // topLevelFieldsIntegers contains documents with several top level fields with integer values. - var topLevelFieldsIntegers = shareddata.NewTopLevelFieldsProvider[string]( + topLevelFieldsIntegers := shareddata.NewTopLevelFieldsProvider[string]( "TopLevelFieldsIntegers", []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, map[string]map[string]any{ From cb6080b80dd113e282344c35f07509fc93c47a18 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 18 Apr 2023 10:55:52 +0100 Subject: [PATCH 40/46] WIP --- integration/query_projection_compat_test.go | 4 ++-- integration/shareddata/shareddata.go | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index e0022c67cd9b..e707e1220d0a 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -44,8 +44,8 @@ func TestQueryProjectionCompat(t *testing.T) { }, map[string]shareddata.Fields{ "int32-two": { - {"foo", int32(1)}, - {"bar", int32(2)}, + {Key: "foo", Value: int32(1)}, + {Key: "bar", Value: int32(2)}, }, }, ) diff --git a/integration/shareddata/shareddata.go b/integration/shareddata/shareddata.go index 482ecf88bf6e..30a40e08b1d9 100644 --- a/integration/shareddata/shareddata.go +++ b/integration/shareddata/shareddata.go @@ -197,14 +197,16 @@ func (values *Values[idType]) IsCompatible(backend string) bool { return slices.Contains(values.backends, backend) } +// field represents a field in a document. type field struct { - Key string Value any + Key string } // Fields is a map of field name -> value. type Fields []field +// NewTopLevelFieldsProvider creates a new TopLevelValues provider. func NewTopLevelFieldsProvider[id comparable](name string, backends []string, validators map[string]map[string]any, data map[id]Fields) *TopLevelValues[id] { return &TopLevelValues[id]{ name: name, From 67b6126b50d7642bd9ad5da44754aeb52439805c Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 18 Apr 2023 11:26:17 +0100 Subject: [PATCH 41/46] WIP --- Taskfile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Taskfile.yml b/Taskfile.yml index 151d9f6b2a2a..b38469ec468c 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -5,7 +5,7 @@ env: GORACE: halt_on_error=1,history_size=2 vars: - INTEGRATIONTIME: 35m + INTEGRATIONTIME: 30m BENCHTIME: 5s FUZZTIME: 15s FUZZCORPUS: ../fuzz-corpus From 81d920a5bdaec3d2047f46fd09a66a9725861050 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 18 Apr 2023 11:29:31 +0100 Subject: [PATCH 42/46] WIP --- internal/handlers/common/projection.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 4e829d8cb677..2d62af20c894 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -148,7 +148,7 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do docWithoutID := doc.DeepCopy() docWithoutID.Remove("_id") - projectedWithoutID, err := projectDoc(docWithoutID, projectionWithoutID, inclusion) + projectedWithoutID, err := projectDocumentWithoutID(docWithoutID, projectionWithoutID, inclusion) if err != nil { return nil, err } @@ -160,7 +160,7 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do return projected, nil } -func projectDoc(doc *types.Document, projection *types.Document, inclusion bool) (*types.Document, error) { +func projectDocumentWithoutID(doc *types.Document, projection *types.Document, inclusion bool) (*types.Document, error) { projected := types.MakeDocument(0) if !inclusion { From 7cdbff8799ab1bfa51d2e5acb20f203df5ffcdca Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 18 Apr 2023 11:32:30 +0100 Subject: [PATCH 43/46] WIP --- internal/handlers/common/projection.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index 2d62af20c894..c9cd1aac3549 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -142,13 +142,7 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do } } - projectionWithoutID := projection.DeepCopy() - projectionWithoutID.Remove("_id") - - docWithoutID := doc.DeepCopy() - docWithoutID.Remove("_id") - - projectedWithoutID, err := projectDocumentWithoutID(docWithoutID, projectionWithoutID, inclusion) + projectedWithoutID, err := projectDocumentWithoutID(doc, projection, inclusion) if err != nil { return nil, err } @@ -160,14 +154,22 @@ func projectDocument(doc, projection *types.Document, inclusion bool) (*types.Do return projected, nil } +// projectDocumentWithoutID applies projection to the copy of the document and returns projected document. +// It ignores _id field in the projection. func projectDocumentWithoutID(doc *types.Document, projection *types.Document, inclusion bool) (*types.Document, error) { + projectionWithoutID := projection.DeepCopy() + projectionWithoutID.Remove("_id") + + docWithoutID := doc.DeepCopy() + docWithoutID.Remove("_id") + projected := types.MakeDocument(0) if !inclusion { - projected = doc.DeepCopy() + projected = docWithoutID.DeepCopy() } - iter := projection.Iterator() + iter := projectionWithoutID.Iterator() defer iter.Close() for { @@ -198,8 +200,8 @@ func projectDocumentWithoutID(doc *types.Document, projection *types.Document, i // process top level fields if path.Len() == 1 { if inclusion { - if doc.Has(key) { - projected.Set(key, must.NotFail(doc.Get(key))) + if docWithoutID.Has(key) { + projected.Set(key, must.NotFail(docWithoutID.Get(key))) } continue From 584e1d9261e9e78aa00e721d27d8f8de3be3fe04 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 18 Apr 2023 14:20:48 +0100 Subject: [PATCH 44/46] WIP --- integration/shareddata/shareddata.go | 1 + 1 file changed, 1 insertion(+) diff --git a/integration/shareddata/shareddata.go b/integration/shareddata/shareddata.go index 30a40e08b1d9..c7cfe84f5ffa 100644 --- a/integration/shareddata/shareddata.go +++ b/integration/shareddata/shareddata.go @@ -276,4 +276,5 @@ func (t *TopLevelValues[id]) IsCompatible(backend string) bool { // check interfaces var ( _ Provider = (*Values[string])(nil) + _ Provider = (*TopLevelValues[string])(nil) ) From cf807cd0441c265b12503b9111f85b2958ed209f Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 18 Apr 2023 21:55:22 +0100 Subject: [PATCH 45/46] WIP --- integration/shareddata/shareddata.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/integration/shareddata/shareddata.go b/integration/shareddata/shareddata.go index cd780f1c92bb..f3e5c630acfb 100644 --- a/integration/shareddata/shareddata.go +++ b/integration/shareddata/shareddata.go @@ -242,7 +242,6 @@ func (b *BenchmarkValues) Docs() iterator.Interface[struct{}, bson.D] { return b.iter } - // field represents a field in a document. type field struct { Value any @@ -321,7 +320,7 @@ func (t *TopLevelValues[id]) IsCompatible(backend string) bool { // check interfaces var ( - _ Provider = (*Values[string])(nil) + _ Provider = (*Values[string])(nil) _ BenchmarkProvider = (*BenchmarkValues)(nil) - _ Provider = (*TopLevelValues[string])(nil) + _ Provider = (*TopLevelValues[string])(nil) ) From 884f43613b6400635f62d90162d1c53e6f295d7b Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 19 Apr 2023 08:56:06 +0100 Subject: [PATCH 46/46] Update integration/shareddata/shareddata.go Co-authored-by: Chi Fujii --- integration/shareddata/shareddata.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/shareddata/shareddata.go b/integration/shareddata/shareddata.go index f3e5c630acfb..6583660c33fa 100644 --- a/integration/shareddata/shareddata.go +++ b/integration/shareddata/shareddata.go @@ -248,7 +248,7 @@ type field struct { Key string } -// Fields is a map of field name -> value. +// Fields is a slice of ordered field name value pair. To avoid fields being inserted in different order between compat and target, use a slice instead of a map. type Fields []field // NewTopLevelFieldsProvider creates a new TopLevelValues provider.