diff options
author | Nick Zolnierz <nicholas.zolnierz@mongodb.com> | 2019-01-02 10:31:29 -0500 |
---|---|---|
committer | Nick Zolnierz <nicholas.zolnierz@mongodb.com> | 2019-01-09 11:17:44 -0500 |
commit | 4b1afd3e3873b8b46804264e38bb39e0423f54dd (patch) | |
tree | 3b35dc944cbba6ec0cb575f58aff385090d889b8 | |
parent | 0e237325a508f3c49eb6a19ea4a9dbb7c6053058 (diff) | |
download | mongo-4b1afd3e3873b8b46804264e38bb39e0423f54dd.tar.gz |
SERVER-37829 Add Stitch library update functions
-rw-r--r-- | src/mongo/db/SConscript | 2 | ||||
-rw-r--r-- | src/mongo/db/field_ref_set.h | 8 | ||||
-rw-r--r-- | src/mongo/db/ops/SConscript | 10 | ||||
-rw-r--r-- | src/mongo/db/ops/parsed_update.cpp | 28 | ||||
-rw-r--r-- | src/mongo/db/ops/parsed_update.h | 13 | ||||
-rw-r--r-- | src/mongo/db/update/update_driver_test.cpp | 6 | ||||
-rw-r--r-- | src/mongo/embedded/stitch_support/SConscript | 2 | ||||
-rw-r--r-- | src/mongo/embedded/stitch_support/stitch_support.cpp | 168 | ||||
-rw-r--r-- | src/mongo/embedded/stitch_support/stitch_support.h | 97 | ||||
-rw-r--r-- | src/mongo/embedded/stitch_support/stitch_support_test.cpp | 148 |
10 files changed, 461 insertions, 21 deletions
diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript index 6d95b8856e5..41bfd7cbaa8 100644 --- a/src/mongo/db/SConscript +++ b/src/mongo/db/SConscript @@ -1137,7 +1137,6 @@ env.Library( 'exec/working_set_common.cpp', 'exec/write_stage_common.cpp', 'ops/parsed_delete.cpp', - 'ops/parsed_update.cpp', 'ops/update_result.cpp', 'pipeline/document_source_cursor.cpp', 'pipeline/document_source_geo_near_cursor.cpp', @@ -1180,6 +1179,7 @@ env.Library( 'index/key_generator', 'logical_session_cache', 'matcher/expressions_mongod_only', + 'ops/parsed_update', 'pipeline/pipeline', 'query/query_common', 'query/query_planner', diff --git a/src/mongo/db/field_ref_set.h b/src/mongo/db/field_ref_set.h index 735683252d2..e64f343c3dd 100644 --- a/src/mongo/db/field_ref_set.h +++ b/src/mongo/db/field_ref_set.h @@ -152,6 +152,14 @@ public: _fieldRefSet.keepShortest(inserted); } + std::vector<std::string> serialize() const { + std::vector<std::string> ret; + for (const auto fieldRef : _fieldRefSet) { + ret.push_back(fieldRef->dottedField().toString()); + } + return ret; + } + bool empty() const { return _fieldRefSet.empty(); } diff --git a/src/mongo/db/ops/SConscript b/src/mongo/db/ops/SConscript index 2218b59ca32..2725c1219d1 100644 --- a/src/mongo/db/ops/SConscript +++ b/src/mongo/db/ops/SConscript @@ -47,6 +47,16 @@ env.Library( ], ) +env.Library( + target='parsed_update', + source='parsed_update.cpp', + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/db/matcher/expressions_mongod_only', + '$BUILD_DIR/mongo/db/update/update_driver', + ], +) + env.CppUnitTest( target='write_ops_parsers_test', source='write_ops_parsers_test.cpp', diff --git a/src/mongo/db/ops/parsed_update.cpp b/src/mongo/db/ops/parsed_update.cpp index 87354810072..56e15faf654 100644 --- a/src/mongo/db/ops/parsed_update.cpp +++ b/src/mongo/db/ops/parsed_update.cpp @@ -65,17 +65,19 @@ Status ParsedUpdate::parseRequest() { _collator = std::move(collator.getValue()); } - Status status = parseArrayFilters(); - if (!status.isOK()) { - return status; + auto statusWithArrayFilters = + parseArrayFilters(_request->getArrayFilters(), _opCtx, _collator.get()); + if (!statusWithArrayFilters.isOK()) { + return statusWithArrayFilters.getStatus(); } + _arrayFilters = std::move(statusWithArrayFilters.getValue()); // We parse the update portion before the query portion because the dispostion of the update // may determine whether or not we need to produce a CanonicalQuery at all. For example, if // the update involves the positional-dollar operator, we must have a CanonicalQuery even if // it isn't required for query execution. parseUpdate(); - status = parseQuery(); + Status status = parseQuery(); if (!status.isOK()) return status; return Status::OK(); @@ -147,15 +149,19 @@ void ParsedUpdate::parseUpdate() { _driver.parse(_request->getUpdates(), _arrayFilters, _request->isMulti()); } -Status ParsedUpdate::parseArrayFilters() { - for (auto rawArrayFilter : _request->getArrayFilters()) { - boost::intrusive_ptr<ExpressionContext> expCtx( - new ExpressionContext(_opCtx, _collator.get())); +StatusWith<std::map<StringData, std::unique_ptr<ExpressionWithPlaceholder>>> +ParsedUpdate::parseArrayFilters(const std::vector<BSONObj>& rawArrayFiltersIn, + OperationContext* opCtx, + CollatorInterface* collator) { + std::map<StringData, std::unique_ptr<ExpressionWithPlaceholder>> arrayFiltersOut; + for (auto rawArrayFilter : rawArrayFiltersIn) { + boost::intrusive_ptr<ExpressionContext> expCtx(new ExpressionContext(opCtx, collator)); auto parsedArrayFilter = MatchExpressionParser::parse(rawArrayFilter, std::move(expCtx), ExtensionsCallbackNoop(), MatchExpressionParser::kBanAllSpecialFeatures); + if (!parsedArrayFilter.isOK()) { return parsedArrayFilter.getStatus().withContext("Error parsing array filter"); } @@ -172,17 +178,17 @@ Status ParsedUpdate::parseArrayFilters() { ErrorCodes::FailedToParse, "Cannot use an expression without a top-level field name in arrayFilters"); } - if (_arrayFilters.find(*fieldName) != _arrayFilters.end()) { + if (arrayFiltersOut.find(*fieldName) != arrayFiltersOut.end()) { return Status(ErrorCodes::FailedToParse, str::stream() << "Found multiple array filters with the same top-level field name " << *fieldName); } - _arrayFilters[*fieldName] = std::move(finalArrayFilter); + arrayFiltersOut[*fieldName] = std::move(finalArrayFilter); } - return Status::OK(); + return std::move(arrayFiltersOut); } PlanExecutor::YieldPolicy ParsedUpdate::yieldPolicy() const { diff --git a/src/mongo/db/ops/parsed_update.h b/src/mongo/db/ops/parsed_update.h index f9a0896a946..5f9ba4d66fd 100644 --- a/src/mongo/db/ops/parsed_update.h +++ b/src/mongo/db/ops/parsed_update.h @@ -62,6 +62,14 @@ class ParsedUpdate { public: /** + * Parses the array filters portion of the update request. + */ + static StatusWith<std::map<StringData, std::unique_ptr<ExpressionWithPlaceholder>>> + parseArrayFilters(const std::vector<BSONObj>& rawArrayFiltersIn, + OperationContext* opCtx, + CollatorInterface* collator); + + /** * Constructs a parsed update. * * The object pointed to by "request" must stay in scope for the life of the constructed @@ -143,11 +151,6 @@ private: */ void parseUpdate(); - /** - * Parses the array filters portion of the update request. - */ - Status parseArrayFilters(); - // Unowned pointer to the transactional context. OperationContext* _opCtx; diff --git a/src/mongo/db/update/update_driver_test.cpp b/src/mongo/db/update/update_driver_test.cpp index 12276487d9f..44a44514d8b 100644 --- a/src/mongo/db/update/update_driver_test.cpp +++ b/src/mongo/db/update/update_driver_test.cpp @@ -561,6 +561,12 @@ public: } }; +TEST_F(ModifiedPathsTestFixture, ReplaceFullDocumentReturnsEmptySet) { + BSONObj spec = fromjson("{a: 1, b: 1}}"); + mutablebson::Document doc(fromjson("{a: 0, b: 0}")); + ASSERT_EQ(getModifiedPaths(&doc, spec), "{}"); +} + TEST_F(ModifiedPathsTestFixture, SetFieldInRoot) { BSONObj spec = fromjson("{$set: {a: 1}}"); mutablebson::Document doc(fromjson("{a: 0}")); diff --git a/src/mongo/embedded/stitch_support/SConscript b/src/mongo/embedded/stitch_support/SConscript index c83d54b09b2..88cf32a79e0 100644 --- a/src/mongo/embedded/stitch_support/SConscript +++ b/src/mongo/embedded/stitch_support/SConscript @@ -52,7 +52,9 @@ stitchSupportTargets = stitchSupportEnv.Library( 'stitch_support.cpp', ], LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/db/index/index_access_methods', '$BUILD_DIR/mongo/db/matcher/expressions', + '$BUILD_DIR/mongo/db/ops/parsed_update', '$BUILD_DIR/mongo/db/query/collation/collator_factory_icu', '$BUILD_DIR/mongo/db/query/collation/collator_factory_interface', ], diff --git a/src/mongo/embedded/stitch_support/stitch_support.cpp b/src/mongo/embedded/stitch_support/stitch_support.cpp index b536f44cb19..5bcd54c50be 100644 --- a/src/mongo/embedded/stitch_support/stitch_support.cpp +++ b/src/mongo/embedded/stitch_support/stitch_support.cpp @@ -37,8 +37,10 @@ #include "mongo/bson/bsonobj.h" #include "mongo/db/client.h" #include "mongo/db/matcher/matcher.h" +#include "mongo/db/ops/parsed_update.h" #include "mongo/db/query/collation/collator_factory_interface.h" #include "mongo/db/service_context.h" +#include "mongo/db/update/update_driver.h" #include "mongo/util/assert_util.h" #include <iostream> @@ -169,6 +171,51 @@ struct stitch_support_v1_matcher { mongo::Matcher matcher; }; +struct stitch_support_v1_update_details { + std::vector<std::string> modifiedPaths; +}; + +struct stitch_support_v1_update { + stitch_support_v1_update(mongo::ServiceContext::UniqueClient client, + mongo::BSONObj updateExpr, + mongo::BSONArray arrayFilters, + stitch_support_v1_matcher* matcher, + stitch_support_v1_collator* collator) + : client(std::move(client)), + opCtx(this->client->makeOperationContext()), + updateExpr(updateExpr.getOwned()), + arrayFilters(arrayFilters.getOwned()), + matcher(matcher), + updateDriver(new mongo::ExpressionContext( + opCtx.get(), collator ? collator->collator.get() : nullptr)) { + std::vector<mongo::BSONObj> arrayFilterVector; + for (auto&& filter : this->arrayFilters) { + arrayFilterVector.push_back(filter.embeddedObject()); + } + this->parsedFilters = uassertStatusOK(mongo::ParsedUpdate::parseArrayFilters( + arrayFilterVector, this->opCtx.get(), collator ? collator->collator.get() : nullptr)); + + // Initializing the update as single-document allows document-replacement updates. + bool multi = false; + + updateDriver.parse(this->updateExpr, parsedFilters, multi); + + uassert(51037, + "Updates with a positional operator require a matcher object.", + matcher || !updateDriver.needMatchDetails()); + } + + mongo::ServiceContext::UniqueClient client; + mongo::ServiceContext::UniqueOperationContext opCtx; + mongo::BSONObj updateExpr; + mongo::BSONArray arrayFilters; + + stitch_support_v1_matcher* matcher; + + std::map<mongo::StringData, std::unique_ptr<mongo::ExpressionWithPlaceholder>> parsedFilters; + mongo::UpdateDriver updateDriver; +}; + namespace mongo { namespace { @@ -246,6 +293,30 @@ stitch_support_v1_matcher* matcher_create(stitch_support_v1_lib* const lib, lib->serviceContext->makeClient("stitch_support"), filter.getOwned(), collator); } +stitch_support_v1_update* update_create(stitch_support_v1_lib* const lib, + BSONObj updateExpr, + BSONArray arrayFilters, + 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 update 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 udpate when the Stitch Support Library is not yet initialized."}; + } + + return new stitch_support_v1_update(lib->serviceContext->makeClient("stitch_support"), + updateExpr, + arrayFilters, + matcher, + collator); +} + int capi_status_get_error(const stitch_support_v1_status* const status) noexcept { invariant(status); return status->statusImpl.error; @@ -336,4 +407,101 @@ int MONGO_API_CALL stitch_support_v1_check_match(stitch_support_v1_matcher* matc }); } +stitch_support_v1_update* MONGO_API_CALL +stitch_support_v1_update_create(stitch_support_v1_lib* lib, + const char* updateExprBSON, + const char* arrayFiltersBSON, + stitch_support_v1_matcher* matcher, + stitch_support_v1_collator* collator, + stitch_support_v1_status* status) { + return enterCXX(mongo::getStatusImpl(status), [&]() { + mongo::BSONObj updateExpr(updateExprBSON); + mongo::BSONArray arrayFilters( + (arrayFiltersBSON ? mongo::BSONObj(arrayFiltersBSON) : mongo::BSONObj())); + return mongo::update_create(lib, updateExpr, arrayFilters, matcher, collator); + }); +} + +void MONGO_API_CALL stitch_support_v1_update_destroy(stitch_support_v1_update* const update) { + mongo::StitchSupportStatusImpl* nullStatus = nullptr; + static_cast<void>(enterCXX(nullStatus, [=]() { delete update; })); +} + +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) { + return enterCXX(mongo::getStatusImpl(status), [&]() { + mongo::BSONObj document(documentBSON); + mongo::StringData matchedField; + + if (update->updateDriver.needMatchDetails()) { + invariant(update->matcher); + + mongo::MatchDetails matchDetails; + matchDetails.requestElemMatchKey(); + bool isMatch = update->matcher->matcher.matches(document, &matchDetails); + invariant(isMatch); + if (matchDetails.hasElemMatchKey()) { + matchedField = matchDetails.elemMatchKey(); + } else { + // Empty 'matchedField' indicates that the matcher did not traverse an array. + } + } + + mongo::mutablebson::Document mutableDoc(document, + mongo::mutablebson::Document::kInPlaceDisabled); + + mongo::FieldRefSet immutablePaths; // Empty set + bool docWasModified = false; + + mongo::FieldRefSetWithStorage modifiedPaths; + + uassertStatusOK(update->updateDriver.update(matchedField, + &mutableDoc, + false /* validateForStorage */, + immutablePaths, + NULL /* 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)); + + uassert( + mongo::ErrorCodes::ExceededMemoryLimit, "Failed to allocate memory for update", output); + + memcpy( + static_cast<void*>(output), static_cast<const void*>(outputObj.objdata()), output_size); + + if (update_details) { + update_details->modifiedPaths = modifiedPaths.serialize(); + } + + return output; + }); +} + +stitch_support_v1_update_details* MONGO_API_CALL stitch_support_v1_update_details_create(void) { + return new stitch_support_v1_update_details; +}; + +void MONGO_API_CALL +stitch_support_v1_update_details_destroy(stitch_support_v1_update_details* update_details) { + delete update_details; +}; + +size_t MONGO_API_CALL stitch_support_v1_update_details_num_modified_paths( + stitch_support_v1_update_details* update_details) { + return update_details->modifiedPaths.size(); +} + +const char* MONGO_API_CALL stitch_support_v1_update_details_path( + stitch_support_v1_update_details* update_details, size_t path_index) { + invariant(path_index < update_details->modifiedPaths.size()); + return update_details->modifiedPaths[path_index].c_str(); +} + } // extern "C" diff --git a/src/mongo/embedded/stitch_support/stitch_support.h b/src/mongo/embedded/stitch_support/stitch_support.h index dcb8942ecda..2e52cbb381a 100644 --- a/src/mongo/embedded/stitch_support/stitch_support.h +++ b/src/mongo/embedded/stitch_support/stitch_support.h @@ -190,6 +190,36 @@ 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. * @@ -206,6 +236,71 @@ stitch_support_v1_status_get_code(const stitch_support_v1_status* status); 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. * @@ -225,7 +320,7 @@ stitch_support_v1_init(stitch_support_v1_status* status); * Returns STITCH_SUPPORT_V1_SUCCESS on success. * * Returns STITCH_SUPPORT_V1_ERROR_LIBRARY_NOT_INITIALIZED and modifies 'status' if - * mongo_embedded_v1_lib_init() has not been called previously. + * stitch_support_v1_lib_init() has not been called previously. */ STITCH_SUPPORT_API int MONGO_API_CALL stitch_support_v1_fini(stitch_support_v1_lib* const lib, stitch_support_v1_status* const status); diff --git a/src/mongo/embedded/stitch_support/stitch_support_test.cpp b/src/mongo/embedded/stitch_support/stitch_support_test.cpp index ea4d024fab5..4b7669f8ea8 100644 --- a/src/mongo/embedded/stitch_support/stitch_support_test.cpp +++ b/src/mongo/embedded/stitch_support/stitch_support_test.cpp @@ -49,6 +49,9 @@ protected: lib = stitch_support_v1_init(status); ASSERT(lib); + + updateDetails = stitch_support_v1_update_details_create(); + ASSERT(updateDetails); } void tearDown() override { @@ -58,10 +61,10 @@ protected: stitch_support_v1_status_destroy(status); status = nullptr; - } - stitch_support_v1_status* status = nullptr; - stitch_support_v1_lib* lib = nullptr; + stitch_support_v1_update_details_destroy(updateDetails); + updateDetails = nullptr; + } auto checkMatch(const char* filterJSON, std::vector<const char*> documentsJSON, @@ -95,6 +98,67 @@ protected: return explanation; } + + void checkUpdate(const char* expr, + const char* document, + mongo::BSONObj expectedResult, + const char* match = nullptr, + const char* arrayFilters = nullptr, + const char* collatorObj = nullptr) { + stitch_support_v1_collator* collator = nullptr; + if (collatorObj) { + collator = + stitch_support_v1_collator_create(lib, fromjson(collatorObj).objdata(), nullptr); + } + + stitch_support_v1_matcher* matcher = nullptr; + if (match) { + matcher = + stitch_support_v1_matcher_create(lib, fromjson(match).objdata(), collator, nullptr); + ASSERT(matcher); + } + + stitch_support_v1_update* update = stitch_support_v1_update_create( + lib, + fromjson(expr).objdata(), + arrayFilters ? fromjson(arrayFilters).objdata() : nullptr, + matcher, + collator, + status); + ASSERT(update); + + char* updateResult = stitch_support_v1_update_apply( + update, fromjson(document).objdata(), updateDetails, status); + ASSERT(updateResult); + + stitch_support_v1_update_destroy(update); + stitch_support_v1_matcher_destroy(matcher); + stitch_support_v1_collator_destroy(collator); + + ASSERT_BSONOBJ_EQ(mongo::BSONObj(updateResult), expectedResult); + + free(updateResult); + } + + const std::string getModifiedPaths() { + ASSERT(updateDetails); + + std::stringstream ss; + ss << "["; + size_t nPaths = stitch_support_v1_update_details_num_modified_paths(updateDetails); + for (size_t pathIdx = 0; pathIdx < nPaths; ++pathIdx) { + auto path = stitch_support_v1_update_details_path(updateDetails, pathIdx); + ss << path; + if (pathIdx != (nPaths - 1)) + ss << ", "; + } + ss << "]"; + return ss.str(); + } + + stitch_support_v1_status* status = nullptr; + stitch_support_v1_lib* lib = nullptr; + stitch_support_v1_update_details* updateDetails = nullptr; }; TEST_F(StitchSupportTest, InitializationIsSuccessful) { @@ -149,6 +213,84 @@ TEST_F(StitchSupportTest, CheckMatchWorksWithCollation) { stitch_support_v1_collator_destroy(collator); } +TEST_F(StitchSupportTest, TestUpdateSingleElement) { + checkUpdate("{$set: {a: 2}}", "{a: 1}", fromjson("{a: 2}")); + ASSERT_EQ(getModifiedPaths(), "[a]"); +} + +TEST_F(StitchSupportTest, TestReplacementStyleUpdateReportsNoModifiedPaths) { + // Replacement-style updates report no modified paths because this functionality is not + // currently needed by Stitch. + checkUpdate("{a: 2}", "{a: 1}", fromjson("{a: 2}")); + ASSERT_EQ(getModifiedPaths(), "[]"); +} + +TEST_F(StitchSupportTest, TestUpdateArrayElement) { + checkUpdate("{$set: {'a.0': 2}}", "{a: [1, 2]}", fromjson("{a: [2, 2]}")); + ASSERT_EQ(getModifiedPaths(), "[a.0]"); + + checkUpdate("{$set: {'a.0.b': 2}}", "{a: [{b: 1}]}", fromjson("{a: [{b: 2}]}")); + ASSERT_EQ(getModifiedPaths(), "[a.0.b]"); +} + +TEST_F(StitchSupportTest, TestUpdateAddToArray) { + checkUpdate("{$set: {'a.1.b': 2}}", "{a: [{b: 1}]}", fromjson("{a: [{b: 1}, {b: 2}]}")); + ASSERT_EQ(getModifiedPaths(), "[a]"); + + checkUpdate( + "{$set: {'a.1.b': 2, c: 3}}", "{a: [{b: 1}]}", fromjson("{a: [{b: 1}, {b: 2}], c: 3}")); + ASSERT_EQ(getModifiedPaths(), "[a, c]"); +} + +TEST_F(StitchSupportTest, TestUpdatePullFromArray) { + checkUpdate("{$pull: {'a': 2}}", "{a: [3, 2, 1]}", fromjson("{a: [3, 1]}")); + ASSERT_EQ(getModifiedPaths(), "[a]"); +} + +TEST_F(StitchSupportTest, TestPositionalUpdates) { + checkUpdate("{$set: {'a.$': 3}}", "{a: [1, 2]}", fromjson("{a: [1, 3]}"), "{a: 2}"); + ASSERT_EQ(getModifiedPaths(), "[a.1]"); + + checkUpdate("{$set: {'a.$.b': 3}}", + "{a: [{b: 1}, {b: 2}]}", + fromjson("{a: [{b: 1}, {b: 3}]}"), + "{'a.b': 2}"); + ASSERT_EQ(getModifiedPaths(), "[a.1.b]"); +} + +TEST_F(StitchSupportTest, TestUpdatesWithArrayFilters) { + checkUpdate( + "{$set: {'a.$[i]': 3}}", "{a: [1, 2]}", fromjson("{a: [1, 3]}"), nullptr, "[{'i': 2}]"); + ASSERT_EQ(getModifiedPaths(), "[a.1]"); + + checkUpdate("{$set: {'a.$[i].b': 3}}", + "{a: [{b: 1}, {b: 2}]}", + fromjson("{a: [{b: 1}, {b: 3}]}"), + nullptr, + "[{'i.b': 2}]"); + ASSERT_EQ(getModifiedPaths(), "[a.1.b]"); +} + +TEST_F(StitchSupportTest, TestUpdateRespectsTheCollation) { + auto caseInsensitive = "{locale: 'en', strength: 2}"; + checkUpdate("{$addToSet: {a: 'santa'}}", + "{a: ['Santa', 'Elf']}", + fromjson("{a: ['Santa', 'Elf']}"), + nullptr, + nullptr, + caseInsensitive); + // $addToSet with existing element is considered a no-op, but the array is marked as modified. + ASSERT_EQ(getModifiedPaths(), "[a]"); + + checkUpdate("{$pull: {a: 'santa'}}", + "{a: ['Santa', 'Elf']}", + fromjson("{a: ['Elf']}"), + nullptr, + nullptr, + caseInsensitive); + ASSERT_EQ(getModifiedPaths(), "[a]"); +} + } // namespace // Define main function as an entry to these tests. |