summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Zolnierz <nicholas.zolnierz@mongodb.com>2019-01-02 10:31:29 -0500
committerNick Zolnierz <nicholas.zolnierz@mongodb.com>2019-01-09 11:17:44 -0500
commit4b1afd3e3873b8b46804264e38bb39e0423f54dd (patch)
tree3b35dc944cbba6ec0cb575f58aff385090d889b8
parent0e237325a508f3c49eb6a19ea4a9dbb7c6053058 (diff)
downloadmongo-4b1afd3e3873b8b46804264e38bb39e0423f54dd.tar.gz
SERVER-37829 Add Stitch library update functions
-rw-r--r--src/mongo/db/SConscript2
-rw-r--r--src/mongo/db/field_ref_set.h8
-rw-r--r--src/mongo/db/ops/SConscript10
-rw-r--r--src/mongo/db/ops/parsed_update.cpp28
-rw-r--r--src/mongo/db/ops/parsed_update.h13
-rw-r--r--src/mongo/db/update/update_driver_test.cpp6
-rw-r--r--src/mongo/embedded/stitch_support/SConscript2
-rw-r--r--src/mongo/embedded/stitch_support/stitch_support.cpp168
-rw-r--r--src/mongo/embedded/stitch_support/stitch_support.h97
-rw-r--r--src/mongo/embedded/stitch_support/stitch_support_test.cpp148
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.