diff options
-rw-r--r-- | src/mongo/db/exec/projection_exec.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/exec/projection_exec.h | 16 | ||||
-rw-r--r-- | src/mongo/embedded/stitch_support/stitch_support.cpp | 120 | ||||
-rw-r--r-- | src/mongo/embedded/stitch_support/stitch_support.h | 290 | ||||
-rw-r--r-- | src/mongo/embedded/stitch_support/stitch_support_test.cpp | 208 |
5 files changed, 504 insertions, 132 deletions
diff --git a/src/mongo/db/exec/projection_exec.cpp b/src/mongo/db/exec/projection_exec.cpp index e1240539f38..84f7fb0fc56 100644 --- a/src/mongo/db/exec/projection_exec.cpp +++ b/src/mongo/db/exec/projection_exec.cpp @@ -226,7 +226,7 @@ StatusWith<BSONObj> ProjectionExec::project(const BSONObj& in, MatchDetails matchDetails; // If it's a positional projection we need a MatchDetails. - if (transformRequiresDetails()) { + if (projectRequiresQueryExpression()) { matchDetails.requestElemMatchKey(); invariant(nullptr != _queryExpression); invariant(_queryExpression->matchesBSON(in, &matchDetails)); diff --git a/src/mongo/db/exec/projection_exec.h b/src/mongo/db/exec/projection_exec.h index 74676d6d1fe..4d79808b0d7 100644 --- a/src/mongo/db/exec/projection_exec.h +++ b/src/mongo/db/exec/projection_exec.h @@ -153,6 +153,15 @@ public: const boost::optional<const double> textScore = boost::none, const int64_t recordId = 0) const; + /** + * Determines if calls to the project method require that this object was created with the full + * query expression. We may need it for MatchDetails. + */ + bool projectRequiresQueryExpression() const { + return ARRAY_OP_POSITIONAL == _arrayOpType; + } + + private: /** * Adds meta fields to the end of a projection. @@ -198,13 +207,6 @@ private: const MatchDetails* details = nullptr) const; /** - * See transform(...) above. - */ - bool transformRequiresDetails() const { - return ARRAY_OP_POSITIONAL == _arrayOpType; - } - - /** * Appends the element 'e' to the builder 'bob', possibly descending into sub-fields of 'e' * if needed. */ diff --git a/src/mongo/embedded/stitch_support/stitch_support.cpp b/src/mongo/embedded/stitch_support/stitch_support.cpp index daa16a8289a..c907c4a9d20 100644 --- a/src/mongo/embedded/stitch_support/stitch_support.cpp +++ b/src/mongo/embedded/stitch_support/stitch_support.cpp @@ -36,6 +36,7 @@ #include "mongo/base/initializer.h" #include "mongo/bson/bsonobj.h" #include "mongo/db/client.h" +#include "mongo/db/exec/projection_exec.h" #include "mongo/db/matcher/matcher.h" #include "mongo/db/ops/parsed_update.h" #include "mongo/db/query/collation/collator_factory_interface.h" @@ -43,7 +44,7 @@ #include "mongo/db/update/update_driver.h" #include "mongo/util/assert_util.h" -#include <iostream> +#include <algorithm> #include <string> #if defined(_WIN32) @@ -162,7 +163,7 @@ struct stitch_support_v1_matcher { stitch_support_v1_collator* collator) : client(std::move(client)), opCtx(this->client->makeOperationContext()), - matcher(filterBSON, + matcher(filterBSON.getOwned(), new mongo::ExpressionContext(opCtx.get(), collator ? collator->collator.get() : nullptr)){}; @@ -171,6 +172,34 @@ struct stitch_support_v1_matcher { mongo::Matcher matcher; }; +struct stitch_support_v1_projection { + stitch_support_v1_projection(mongo::ServiceContext::UniqueClient client, + const mongo::BSONObj& pattern, + stitch_support_v1_matcher* matcher, + stitch_support_v1_collator* collator) + : client(std::move(client)), + opCtx(this->client->makeOperationContext()), + projectionExec(opCtx.get(), + pattern.getOwned(), + matcher ? matcher->matcher.getMatchExpression() : nullptr, + collator ? collator->collator.get() : nullptr), + matcher(matcher) { + uassert(51050, + "Projections with a positional operator require a matcher", + matcher || !projectionExec.projectRequiresQueryExpression()); + uassert(51051, + "$textScore, $sortKey, $recordId, $geoNear and $returnKey are not allowed in this " + "context", + !projectionExec.hasMetaFields() && !projectionExec.returnKey()); + } + + mongo::ServiceContext::UniqueClient client; + mongo::ServiceContext::UniqueOperationContext opCtx; + mongo::ProjectionExec projectionExec; + + stitch_support_v1_matcher* matcher; +}; + struct stitch_support_v1_update_details { std::vector<std::string> modifiedPaths; }; @@ -290,7 +319,28 @@ stitch_support_v1_matcher* matcher_create(stitch_support_v1_lib* const lib, } return new stitch_support_v1_matcher( - lib->serviceContext->makeClient("stitch_support"), filter.getOwned(), collator); + lib->serviceContext->makeClient("stitch_support"), filter, collator); +} + +stitch_support_v1_projection* projection_create(stitch_support_v1_lib* const lib, + BSONObj spec, + stitch_support_v1_matcher* matcher, + stitch_support_v1_collator* collator) { + if (!library) { + throw StitchSupportException{STITCH_SUPPORT_V1_ERROR_LIBRARY_NOT_INITIALIZED, + "Cannot create a new projection when the Stitch Support " + "Library is not yet initialized."}; + } + + if (library.get() != lib) { + throw StitchSupportException{STITCH_SUPPORT_V1_ERROR_INVALID_LIB_HANDLE, + "Cannot create a new projection when the Stitch Support " + "Library is not yet initialized."}; + } + + + return new stitch_support_v1_projection( + lib->serviceContext->makeClient("stitch_support"), spec, matcher, collator); } stitch_support_v1_update* update_create(stitch_support_v1_lib* const lib, @@ -385,8 +435,8 @@ stitch_support_v1_matcher* MONGO_API_CALL stitch_support_v1_matcher_create(stitch_support_v1_lib* lib, const char* filterBSON, stitch_support_v1_collator* collator, - stitch_support_v1_status* const statusPtr) { - return enterCXX(mongo::getStatusImpl(statusPtr), [&]() { + stitch_support_v1_status* const status) { + return enterCXX(mongo::getStatusImpl(status), [&]() { mongo::BSONObj filter(filterBSON); return mongo::matcher_create(lib, filter, collator); }); @@ -397,16 +447,55 @@ void MONGO_API_CALL stitch_support_v1_matcher_destroy(stitch_support_v1_matcher* static_cast<void>(enterCXX(nullStatus, [=]() { delete matcher; })); } +stitch_support_v1_projection* MONGO_API_CALL +stitch_support_v1_projection_create(stitch_support_v1_lib* lib, + const char* specBSON, + stitch_support_v1_matcher* matcher, + stitch_support_v1_collator* collator, + stitch_support_v1_status* const status) { + return enterCXX(mongo::getStatusImpl(status), [&]() { + mongo::BSONObj spec(specBSON); + return mongo::projection_create(lib, spec, matcher, collator); + }); +} + +void MONGO_API_CALL +stitch_support_v1_projection_destroy(stitch_support_v1_projection* const projection) { + mongo::StitchSupportStatusImpl* nullStatus = nullptr; + static_cast<void>(enterCXX(nullStatus, [=]() { delete projection; })); +} + int MONGO_API_CALL stitch_support_v1_check_match(stitch_support_v1_matcher* matcher, const char* documentBSON, bool* isMatch, - stitch_support_v1_status* statusPtr) { - return enterCXX(mongo::getStatusImpl(statusPtr), [&]() { + stitch_support_v1_status* status) { + return enterCXX(mongo::getStatusImpl(status), [&]() { mongo::BSONObj document(documentBSON); *isMatch = matcher->matcher.matches(document, nullptr); }); } +char* MONGO_API_CALL +stitch_support_v1_projection_apply(stitch_support_v1_projection* const projection, + const char* documentBSON, + stitch_support_v1_status* status) { + return enterCXX(mongo::getStatusImpl(status), [&]() { + mongo::BSONObj document(documentBSON); + + auto outputResult = projection->projectionExec.project(document); + auto outputObj = uassertStatusOK(outputResult); + auto outputSize = static_cast<size_t>(outputObj.objsize()); + auto output = new (std::nothrow) char[outputSize]; + + uassert(mongo::ErrorCodes::ExceededMemoryLimit, + "Failed to allocate memory for projection", + output); + + static_cast<void>(std::copy_n(outputObj.objdata(), outputSize, output)); + return output; + }); +} + stitch_support_v1_update* MONGO_API_CALL stitch_support_v1_update_create(stitch_support_v1_lib* lib, const char* updateExprBSON, @@ -462,19 +551,18 @@ stitch_support_v1_update_apply(stitch_support_v1_update* const update, &mutableDoc, false /* validateForStorage */, immutablePaths, - NULL /* logOpRec*/, + nullptr /* logOpRec*/, &docWasModified, &modifiedPaths)); auto outputObj = mutableDoc.getObject(); - size_t output_size = static_cast<size_t>(outputObj.objsize()); - char* output = static_cast<char*>(malloc(output_size)); + size_t outputSize = static_cast<size_t>(outputObj.objsize()); + auto output = new (std::nothrow) char[outputSize]; uassert( mongo::ErrorCodes::ExceededMemoryLimit, "Failed to allocate memory for update", output); - memcpy( - static_cast<void*>(output), static_cast<const void*>(outputObj.objdata()), output_size); + static_cast<void>(std::copy_n(outputObj.objdata(), outputSize, output)); if (update_details) { update_details->modifiedPaths = modifiedPaths.serialize(); @@ -490,7 +578,8 @@ stitch_support_v1_update_details* MONGO_API_CALL stitch_support_v1_update_detail void MONGO_API_CALL stitch_support_v1_update_details_destroy(stitch_support_v1_update_details* update_details) { - delete update_details; + mongo::StitchSupportStatusImpl* nullStatus = nullptr; + static_cast<void>(enterCXX(nullStatus, [=]() { delete update_details; })); }; size_t MONGO_API_CALL stitch_support_v1_update_details_num_modified_paths( @@ -504,4 +593,9 @@ const char* MONGO_API_CALL stitch_support_v1_update_details_path( return update_details->modifiedPaths[path_index].c_str(); } +void MONGO_API_CALL stitch_support_v1_bson_free(char* bson) { + mongo::StitchSupportStatusImpl* nullStatus = nullptr; + static_cast<void>(enterCXX(nullStatus, [=]() { delete[](bson); })); +} + } // extern "C" diff --git a/src/mongo/embedded/stitch_support/stitch_support.h b/src/mongo/embedded/stitch_support/stitch_support.h index 2e52cbb381a..c75dc732820 100644 --- a/src/mongo/embedded/stitch_support/stitch_support.h +++ b/src/mongo/embedded/stitch_support/stitch_support.h @@ -190,36 +190,6 @@ stitch_support_v1_status_get_explanation(const stitch_support_v1_status* status) STITCH_SUPPORT_API int MONGO_API_CALL stitch_support_v1_status_get_code(const stitch_support_v1_status* status); -typedef struct stitch_support_v1_update_details stitch_support_v1_update_details; - -/** - * Create an "update details" object to pass to stitch_support_v1_update_apply(), which will - * populate the update details with a list of paths modified by the update. - * - * Clients can reuse the same update details object for multiple calls to - * stitch_support_v1_update_apply(). - */ -STITCH_SUPPORT_API stitch_support_v1_update_details* MONGO_API_CALL -stitch_support_v1_update_details_create(void); - -STITCH_SUPPORT_API void MONGO_API_CALL -stitch_support_v1_update_details_destroy(stitch_support_v1_update_details* update_details); - -/** - * The number of modified paths in an update details object. Always call this function to ensure an - * index is in bounds before calling stitch_support_v1_update_details_path(). - */ -STITCH_SUPPORT_API size_t MONGO_API_CALL stitch_support_v1_update_details_num_modified_paths( - stitch_support_v1_update_details* update_details); - -/** - * Return a dotted-path string from the given index of the modified paths in the update details - * object. The above note about distinguishing field names from array indexes in the documentation - * of stitch_support_v1_match_details_elem_match_path_component() also applies here. - */ -STITCH_SUPPORT_API const char* MONGO_API_CALL stitch_support_v1_update_details_path( - stitch_support_v1_update_details* update_details, size_t path_index); - /** * An object which describes the runtime state of the Stitch Support Library. * @@ -236,71 +206,6 @@ STITCH_SUPPORT_API const char* MONGO_API_CALL stitch_support_v1_update_details_p typedef struct stitch_support_v1_lib stitch_support_v1_lib; /** - * An update object used to apply an update to a BSON document, which may modify particular - * fields (e.g.: {$set: {a: 1}}) or replace the entire document with a new one. - */ -typedef struct stitch_support_v1_update stitch_support_v1_update; - -typedef struct stitch_support_v1_matcher stitch_support_v1_matcher; -typedef struct stitch_support_v1_collator stitch_support_v1_collator; -/** - * Creates a stitch_support_v1_update object by parsing the given update expression. - * - * If the update expression includes a positional ($) operator, then the caller must pass a - * stitch_support_v1_matcher, which is used to determine which array element matches the positional. - * The 'matcher' argument is not used when the update expression has no positional operator, and it - * can be NULL. - * - * The caller can optionally provide a collator, which is used when evaluating arrayFilters match - * expressions. The 'collator' parameter must match the collator in 'matcher'. A mismatch will raise - * an invariant violation. Multiple matcher, projection, and update objects can share the same - * collation object. - * - * The newly created update object does _not_ take ownership of its 'matcher' or 'collators' - * objects. The client is responsible for ensuring that the matcher and collator continue to exist - * for the lifetime of the update and for ultimately destroying all three of the update, matcher, - * and collator. - */ -STITCH_SUPPORT_API stitch_support_v1_update* MONGO_API_CALL -stitch_support_v1_update_create(stitch_support_v1_lib* lib, - const char* updateBSON, - const char* arrayFiltersBSON, - stitch_support_v1_matcher* matcher, - stitch_support_v1_collator* collator, - stitch_support_v1_status* status); - -/** - * Destroys a valid stitch_support_v1_update object. - * - * This function does not destroy the collator associated with the destroyed update. When - * destroying a update and its associated collator together, it is safe to destroy them in either - * order. Although a update is no longer valid once its associated collator has been destroyed, it - * is still safe to call this destroy function on the update. - * - * This function is not thread safe, and it must not execute concurrently with any other function - * that accesses the matcher object being destroyed. - * - * This function does not report failures. - */ -STITCH_SUPPORT_API void MONGO_API_CALL -stitch_support_v1_update_destroy(stitch_support_v1_update* const update); - -/** - * Apply an update to an input document, writing the resulting BSON to the 'output' buffer. Returns - * a pointer to the output buffer on success or NULL on error. The caller is responsible for - * destroying the result buffer with free(). - * - * If the update includes a positional ($) operator, the caller should verify before applying it - * that the associated matcher matches the input document. A non-matching input document will - * trigger an assertion failure. - */ -STITCH_SUPPORT_API char* MONGO_API_CALL -stitch_support_v1_update_apply(stitch_support_v1_update* const update, - const char* documentBSON, - stitch_support_v1_update_details* update_details, - stitch_support_v1_status* status); - -/** * Creates a stitch_support_v1_lib object, which stores context for the Stitch Support library. A * process should only ever have one stitch_support_v1_lib instance. * @@ -415,6 +320,201 @@ stitch_support_v1_check_match(stitch_support_v1_matcher* matcher, bool* isMatch, stitch_support_v1_status* status); +/** + * A projection object is used to apply a projection to a BSON document which may include or exclude + * particular fields (e.g.: {_id: 0, a: 1}) or produce new columns through an expression made of + * projection operators. + * + * A projection requires a matcher if it uses the positional ($) operator. This matcher is used to + * determine which array element matches the specified position. The matcher is optional and unused + * when there is no positional operator. + * + * A projection can optionally use a collator for evaluating $elemMatch operators. Projection + * objects without collators will use the default collation for $elemMatch, even if their matcher + * has a collator object. Multiple matcher, projection, and update objects can share the same + * collation object. + */ +typedef struct stitch_support_v1_projection stitch_support_v1_projection; + +/** + * Creates a stitch_support_v1_projection object, which is used to apply a projection to a BSON + * document. The projection specification is also represented as a BSON document, which is passed in + * the 'specBSON' argument. The syntax used for projection is the same as a MongoDB "find" command + * (i.e., not an aggregation $project stage). + * + * This function will fail if the projection specification is invalid, returning NULL and populating + * 'status' with information about the error. + * + * If the projection specification includes a positional ($) operator, then the caller must pass a + * mongo_embedded_v1_matcher. The 'matcher' argument is unnecessary if the specification has no + * positional operator and it can be NULL. + * + * The 'collator' argument, a pointer to a stitch_support_v1_collator, will cause the projection to + * use the given collator if provided, The pointer can be NULL to cause the projection to use no + * collator. + * + * The newly created projection object does _not_ take ownership of its 'matcher' or 'collator' + * objects. The client is responsible for ensuring that the matcher and collator continue to exist + * for the lifetime of the projection and for ultimately destroying all three of the projection, + * matcher and collator. + */ +STITCH_SUPPORT_API stitch_support_v1_projection* MONGO_API_CALL +stitch_support_v1_projection_create(stitch_support_v1_lib* lib, + const char* specBSON, + stitch_support_v1_matcher* matcher, + stitch_support_v1_collator* collator, + stitch_support_v1_status* status); + +/** + * Destroys a valid stitch_support_v1_projection object. + * + * This function does not destroy the collator or matcher associated with the destroyed projection. + * When destroying a projection and its associated collator and/or matcher together, it is safe to + * destroy them in any order. Although a projection is no longer valid once its associated collator + * or matcher has been destroyed, it is still safe to call this destroy function on the projection. + * + * This function is not thread safe, and it must not execute concurrently with any other function + * that accesses the matcher object being destroyed. + * + * This function does not report failures. + */ +STITCH_SUPPORT_API void MONGO_API_CALL +stitch_support_v1_projection_destroy(stitch_support_v1_projection* const projection); + +/** + * Apply a projection to an input document, writing the resulting BSON to a newly allocated 'output' + * buffer. Returns a pointer to the output buffer on success or NULL on error. 'status' is populated + * in the error case. The caller is responsible for destroying the result buffer with + * stitch_support_v1_bson_free(). + * + * If the projection includes a positional ($) operator, the caller should verify before applying it + * that the associated matcher matches the input document. A non-matching input document will + * trigger an assertion failure. + */ +STITCH_SUPPORT_API char* MONGO_API_CALL +stitch_support_v1_projection_apply(stitch_support_v1_projection* const projection, + const char* documentBSON, + stitch_support_v1_status* status); + +/** + * An update details object stores the list of paths modified by a call to + * stitch_support_v1_update_apply(). + */ +typedef struct stitch_support_v1_update_details stitch_support_v1_update_details; + +/** + * Create an "update details" object to pass to stitch_support_v1_update_apply(), which will + * populate the update details with a list of paths modified by the update. + * + * Clients can reuse the same update details object for multiple calls to + * stitch_support_v1_update_apply(). + */ +STITCH_SUPPORT_API stitch_support_v1_update_details* MONGO_API_CALL +stitch_support_v1_update_details_create(void); + +/** + * Destroys an update details object. + * + * This function is not thread safe, and it must not execute concurrently with any other function + * that accesses the update details object being destroyed. + * + * This function does not report failures. + */ +STITCH_SUPPORT_API void MONGO_API_CALL +stitch_support_v1_update_details_destroy(stitch_support_v1_update_details* update_details); + +/** + * The number of modified paths in an update details object. Always call this function to ensure an + * index is in bounds before calling stitch_support_v1_update_details_path(). + */ +STITCH_SUPPORT_API size_t MONGO_API_CALL stitch_support_v1_update_details_num_modified_paths( + stitch_support_v1_update_details* update_details); + +/** + * Return a dotted-path string from the given index of the modified paths in the update details + * object. + */ +STITCH_SUPPORT_API const char* MONGO_API_CALL stitch_support_v1_update_details_path( + stitch_support_v1_update_details* update_details, size_t path_index); + +/** + * An update object used to apply an update to a BSON document, which may modify particular + * fields (e.g.: {$set: {a: 1}}) or replace the entire document with a new one. + */ +typedef struct stitch_support_v1_update stitch_support_v1_update; + +/** + * Creates a stitch_support_v1_update object by parsing the update expression provided in the + * 'updateBSON' argument. + * + * The optional 'arrayFiltersBSON' argument must be a BSON array where each element of the array + * represents an array filter with the field name storing the placeholder name and the value storing + * the assoicated match expression. This argument is permitted to be NULL in the case where there + * are no array filters. If 'updateBSON' contains a placeholder ($[*]) that does not have a + * corresponding 'arrayFiltersBSON' entry this function will fail to parse. + * + * This function will fail if either 'updateBSON' or 'arrayFiltersBSON fails to parse, returning + * NULL and populating 'status' with information about the error. + * + * The caller can optionally provide a collator, which is used when evaluating arrayFilters match + * expressions. The 'collator' parameter must match the collator in 'matcher'. A mismatch will raise + * an invariant violation if 'matcher' is non-NULL. Multiple matcher, projection, and update objects + * can share the same collation object. + * + * The newly created update object does _not_ take ownership of its 'matcher' or 'collator' + * objects. The client is responsible for ensuring that the matcher and collator continue to exist + * for the lifetime of the update and for ultimately destroying all three of the update, matcher, + * and collator. + */ +STITCH_SUPPORT_API stitch_support_v1_update* MONGO_API_CALL +stitch_support_v1_update_create(stitch_support_v1_lib* lib, + const char* updateBSON, + const char* arrayFiltersBSON, + stitch_support_v1_matcher* matcher, + stitch_support_v1_collator* collator, + stitch_support_v1_status* status); + +/** + * Destroys a valid stitch_support_v1_update object. + * + * This function does not destroy the collator associated with the destroyed update. When + * destroying an update and its associated collator together, it is safe to destroy them in either + * order. Although an update is no longer valid once its associated collator has been destroyed, it + * is still safe to call this destroy function on the update. + * + * This function is not thread safe, and it must not execute concurrently with any other function + * that accesses the update object being destroyed. + * + * This function does not report failures. + */ +STITCH_SUPPORT_API void MONGO_API_CALL +stitch_support_v1_update_destroy(stitch_support_v1_update* const update); + +/** + * Apply an update to an input document writing the resulting BSON to a newly allocated output + * buffer. Returns a pointer to the output buffer on success or NULL on error. 'status' is populated + * in the error case. The caller is responsible for destroying the result buffer with + * stitch_support_v1_bson_free(). + * + * If the update includes a positional ($) operator, the caller should verify before applying it + * that the associated matcher matches the input document. A non-matching input document will + * trigger an assertion failure. + */ +STITCH_SUPPORT_API char* MONGO_API_CALL +stitch_support_v1_update_apply(stitch_support_v1_update* const update, + const char* documentBSON, + stitch_support_v1_update_details* update_details, + stitch_support_v1_status* status); + +/** + * Free the memory of a BSON buffer returned by stitch_support_v1_projection_apply() or + * stitch_support_v1_update_apply(). This function can be safely called on a NULL pointer. + * + * This function can be called at any time to deallocate a BSON buffer and will not invalidate any + * library object. + */ +STITCH_SUPPORT_API void MONGO_API_CALL stitch_support_v1_bson_free(char* bson); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/mongo/embedded/stitch_support/stitch_support_test.cpp b/src/mongo/embedded/stitch_support/stitch_support_test.cpp index ba43a9267c1..456a080799c 100644 --- a/src/mongo/embedded/stitch_support/stitch_support_test.cpp +++ b/src/mongo/embedded/stitch_support/stitch_support_test.cpp @@ -28,6 +28,7 @@ * it in the license file. */ +#include <algorithm> #include <string> #include <utility> @@ -43,6 +44,7 @@ namespace { using mongo::fromjson; using mongo::makeGuard; using mongo::ScopeGuard; +using mongo::tojson; class StitchSupportTest : public mongo::unittest::Test { protected: @@ -75,33 +77,104 @@ protected: auto matcher = stitch_support_v1_matcher_create( lib, fromjson(filterJSON).objdata(), collator, nullptr); ASSERT(matcher); - bool isMatch = true; - for (const auto documentJSON : documentsJSON) { - stitch_support_v1_check_match( - matcher, fromjson(documentJSON).objdata(), &isMatch, nullptr); - } - stitch_support_v1_matcher_destroy(matcher); - return isMatch; + ON_BLOCK_EXIT([matcher] { stitch_support_v1_matcher_destroy(matcher); }); + return std::all_of( + documentsJSON.begin(), documentsJSON.end(), [=](const char* documentJSON) { + bool isMatch; + stitch_support_v1_check_match( + matcher, fromjson(documentJSON).objdata(), &isMatch, nullptr); + return isMatch; + }); } auto checkMatchStatus(const char* filterJSON, const char* documentJSON, stitch_support_v1_collator* collator = nullptr) { - auto match_status = stitch_support_v1_status_create(); + auto matchStatus = stitch_support_v1_status_create(); + ON_BLOCK_EXIT([matchStatus] { stitch_support_v1_status_destroy(matchStatus); }); auto matcher = stitch_support_v1_matcher_create( - lib, fromjson(filterJSON).objdata(), collator, match_status); - ASSERT(!matcher); + lib, fromjson(filterJSON).objdata(), collator, matchStatus); + if (matcher) { + stitch_support_v1_matcher_destroy(matcher); + FAIL("Expected stich_support_v1_matcher_create to fail"); + } ASSERT_EQ(STITCH_SUPPORT_V1_ERROR_EXCEPTION, - stitch_support_v1_status_get_error(match_status)); + stitch_support_v1_status_get_error(matchStatus)); // Make sure that we get a proper code back but don't worry about its exact value. - ASSERT_NE(0, stitch_support_v1_status_get_code(match_status)); - std::string explanation(stitch_support_v1_status_get_explanation(match_status)); - stitch_support_v1_status_destroy(match_status); + ASSERT_NE(0, stitch_support_v1_status_get_code(matchStatus)); + std::string explanation(stitch_support_v1_status_get_explanation(matchStatus)); return explanation; } + auto checkProjection(const char* specJSON, + std::vector<const char*> documentsJSON, + const char* filterJSON = nullptr, + stitch_support_v1_collator* collator = nullptr, + bool denyProjectionCollator = false) { + stitch_support_v1_matcher* matcher = nullptr; + if (filterJSON) { + matcher = stitch_support_v1_matcher_create( + lib, fromjson(filterJSON).objdata(), collator, nullptr); + ASSERT(matcher); + } + ON_BLOCK_EXIT([matcher] { stitch_support_v1_matcher_destroy(matcher); }); + + auto projection = + stitch_support_v1_projection_create(lib, + fromjson(specJSON).objdata(), + matcher, + denyProjectionCollator ? nullptr : collator, + nullptr); + ASSERT(projection); + ON_BLOCK_EXIT([projection] { stitch_support_v1_projection_destroy(projection); }); + + std::vector<std::string> results; + std::transform(documentsJSON.begin(), + documentsJSON.end(), + std::back_inserter(results), + [=](const char* documentJSON) { + auto bson = stitch_support_v1_projection_apply( + projection, fromjson(documentJSON).objdata(), nullptr); + auto result = tojson(mongo::BSONObj(bson)); + stitch_support_v1_bson_free(bson); + return result; + }); + + return results; + } + + auto checkProjectionStatus(const char* specJSON, + const char* documentJSON, + const char* filterJSON = nullptr, + stitch_support_v1_collator* collator = nullptr) { + auto projectionStatus = stitch_support_v1_status_create(); + ON_BLOCK_EXIT([projectionStatus] { stitch_support_v1_status_destroy(projectionStatus); }); + + stitch_support_v1_matcher* matcher = nullptr; + if (filterJSON) { + matcher = stitch_support_v1_matcher_create( + lib, fromjson(filterJSON).objdata(), collator, nullptr); + ASSERT(matcher); + } + ON_BLOCK_EXIT([matcher] { stitch_support_v1_matcher_destroy(matcher); }); + + auto projection = stitch_support_v1_projection_create( + lib, fromjson(specJSON).objdata(), matcher, collator, projectionStatus); + if (projection) { + stitch_support_v1_projection_destroy(projection); + FAIL("Expected stich_support_v1_projection_create to fail"); + } + + ASSERT_EQ(STITCH_SUPPORT_V1_ERROR_EXCEPTION, + stitch_support_v1_status_get_error(projectionStatus)); + // Make sure that we get a proper code back but don't worry about its exact value. + ASSERT_NE(0, stitch_support_v1_status_get_code(projectionStatus)); + + return std::string(stitch_support_v1_status_get_explanation(projectionStatus)); + } + void checkUpdate(const char* expr, const char* document, mongo::BSONObj expectedResult, @@ -139,11 +212,56 @@ protected: << stitch_support_v1_status_get_error(status) << ":" << stitch_support_v1_status_get_explanation(status); ASSERT(updateResult); - ON_BLOCK_EXIT([updateResult] { free(updateResult); }); + ON_BLOCK_EXIT([updateResult] { stitch_support_v1_bson_free(updateResult); }); ASSERT_BSONOBJ_EQ(mongo::BSONObj(updateResult), expectedResult); } + auto checkUpdateStatus(const char* expr, + const char* document, + const char* match = nullptr, + const char* arrayFilters = nullptr, + const char* collatorObj = nullptr) { + auto updateStatus = stitch_support_v1_status_create(); + ON_BLOCK_EXIT([updateStatus] { stitch_support_v1_status_destroy(updateStatus); }); + + stitch_support_v1_collator* collator = nullptr; + if (collatorObj) { + collator = + stitch_support_v1_collator_create(lib, fromjson(collatorObj).objdata(), nullptr); + } + ON_BLOCK_EXIT([collator] { stitch_support_v1_collator_destroy(collator); }); + + stitch_support_v1_matcher* matcher = nullptr; + if (match) { + matcher = + stitch_support_v1_matcher_create(lib, fromjson(match).objdata(), collator, nullptr); + ASSERT(matcher); + } + ON_BLOCK_EXIT([matcher] { stitch_support_v1_matcher_destroy(matcher); }); + + stitch_support_v1_update* update = stitch_support_v1_update_create( + lib, + fromjson(expr).objdata(), + arrayFilters ? fromjson(arrayFilters).objdata() : nullptr, + matcher, + collator, + updateStatus); + if (!update) { + ASSERT_EQ(STITCH_SUPPORT_V1_ERROR_EXCEPTION, + stitch_support_v1_status_get_error(updateStatus)); + // Make sure that we get a proper code back but don't worry about its exact value. + ASSERT_NE(0, stitch_support_v1_status_get_code(updateStatus)); + } else { + ON_BLOCK_EXIT([update] { stitch_support_v1_update_destroy(update); }); + char* updateResult = stitch_support_v1_update_apply( + update, fromjson(document).objdata(), updateDetails, updateStatus); + ASSERT_NE(0, stitch_support_v1_status_get_code(updateStatus)); + ASSERT(!updateResult); + } + return std::string(stitch_support_v1_status_get_explanation(updateStatus)); + } + const std::string getModifiedPaths() { ASSERT(updateDetails); @@ -213,8 +331,53 @@ TEST_F(StitchSupportTest, CheckMatchWorksWithStatus) { TEST_F(StitchSupportTest, CheckMatchWorksWithCollation) { auto collator = stitch_support_v1_collator_create( lib, fromjson("{locale: 'en', strength: 2}").objdata(), nullptr); + ON_BLOCK_EXIT([collator] { stitch_support_v1_collator_destroy(collator); }); ASSERT_TRUE(checkMatch("{a: 'word'}", {"{a: 'WORD', b: 'other'}"}, collator)); - stitch_support_v1_collator_destroy(collator); +} + +TEST_F(StitchSupportTest, CheckProjectionWorkDefaults) { + auto results = + checkProjection("{a: 1}", {"{_id: 1, a: 100, b: 200}", "{_id: 1, a: 200, b: 300}"}); + ASSERT_EQ("{ \"_id\" : 1, \"a\" : 100 }", results[0]); + ASSERT_EQ("{ \"_id\" : 1, \"a\" : 200 }", results[1]); + results = checkProjection("{'a.$.c': 1}", + {"{_id: 1, a: [{b: 2, c: 100}, {b: 1, c: 200}]}", + "{_id: 1, a: [{b: 1, c: 100, d: 45}, {b: 2, c: 200}]}"}, + "{'a.b': 1}"); + ASSERT_EQ("{ \"_id\" : 1, \"a\" : [ { \"b\" : 1, \"c\" : 200 } ] }", results[0]); + ASSERT_EQ("{ \"_id\" : 1, \"a\" : [ { \"b\" : 1, \"c\" : 100, \"d\" : 45 } ] }", results[1]); + ASSERT_EQ( + "{ \"a\" : [ { \"b\" : 2, \"c\" : 2 } ] }", + checkProjection("{a: {$elemMatch: {b: 2}}}", {"{a: [{b: 1, c: 1}, {b: 2, c: 2}]}"})[0]); + ASSERT_EQ("{ \"a\" : [ 2, 3 ] }", + checkProjection("{a: {$slice: [1, 2]}}", {"{a: [1, 2, 3, 4]}"})[0]); +} + +TEST_F(StitchSupportTest, CheckProjectionProducesExpectedStatus) { + ASSERT_EQ( + "Projections with a positional operator require a matcher", + checkProjectionStatus("{'a.$.c': 1}", "{_id: 1, a: [{b: 2, c: 100}, {b: 1, c: 200}]}")); + ASSERT_EQ( + "$textScore, $sortKey, $recordId, $geoNear and $returnKey are not allowed in this context", + checkProjectionStatus("{a: {$meta: 'textScore'}}", "{_id: 1, a: 100, b: 200}")); +} + +TEST_F(StitchSupportTest, CheckProjectionCollatesRespectfully) { + auto collator = stitch_support_v1_collator_create( + lib, fromjson("{locale: 'en', strength: 2}").objdata(), nullptr); + ON_BLOCK_EXIT([collator] { stitch_support_v1_collator_destroy(collator); }); + ASSERT_EQ("{ \"_id\" : 1, \"a\" : [ \"mixEdCaSe\" ] }", + checkProjection("{a: {$elemMatch: {$eq: 'MiXedcAse'}}}", + {"{_id: 1, a: ['lowercase', 'mixEdCaSe', 'UPPERCASE']}"}, + nullptr, + collator)[0]); + // Ignore a matcher's collator. + ASSERT_EQ("{ \"_id\" : 1 }", + checkProjection("{a: {$elemMatch: {$eq: 'MiXedcAse'}}}", + {"{_id: 1, a: ['lowercase', 'mixEdCaSe', 'UPPERCASE']}"}, + "{_id: 1}", + collator, + true)[0]); } TEST_F(StitchSupportTest, TestUpdateSingleElement) { @@ -295,6 +458,19 @@ TEST_F(StitchSupportTest, TestUpdateRespectsTheCollation) { ASSERT_EQ(getModifiedPaths(), "[a]"); } +TEST_F(StitchSupportTest, TestUpdateProducesProperStatus) { + ASSERT_EQ("Unknown modifier: $bogus", checkUpdateStatus("{$bogus: {a: 2}}", "{a: 1}")); + ASSERT_EQ("Updating the path 'a' would create a conflict at 'a'", + checkUpdateStatus("{$set: {a: 2, a: 3}}", "{a: 1}")); + ASSERT_EQ("No array filter found for identifier 'i' in path 'a.$[i]'", + checkUpdateStatus("{$set: {'a.$[i]': 3}}", "{a: [1, 2]}")); + ASSERT_EQ("No array filter found for identifier 'i' in path 'a.$[i]'", + checkUpdateStatus("{$set: {'a.$[i]': 3}}", "{a: [1, 2]}", nullptr, "[{'j': 2}]")); + ASSERT_EQ("Update created a conflict at 'a.0'", + checkUpdateStatus( + "{$set: {'a.$[i]': 2, 'a.$[j]': 3}}", "{a: [0]}", nullptr, " [{i: 0}, {j:0}]")); +} + } // namespace // Define main function as an entry to these tests. |