Skip to content

Implement PatchMutation::ApplyToLocalView #1973

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions Firestore/core/src/firebase/firestore/model/mutation.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <utility>

#include "Firestore/core/src/firebase/firestore/model/document.h"
#include "Firestore/core/src/firebase/firestore/model/field_path.h"
#include "Firestore/core/src/firebase/firestore/util/hard_assert.h"

namespace firebase {
Expand Down Expand Up @@ -67,6 +68,58 @@ std::shared_ptr<const MaybeDocument> SetMutation::ApplyToLocalView(
/*has_local_mutations=*/true);
}

PatchMutation::PatchMutation(DocumentKey&& key,
FieldValue&& value,
FieldMask&& mask,
Precondition&& precondition)
: Mutation(std::move(key), std::move(precondition)),
value_(std::move(value)),
mask_(std::move(mask)) {
}

std::shared_ptr<const MaybeDocument> PatchMutation::ApplyToLocalView(
const std::shared_ptr<const MaybeDocument>& maybe_doc,
const MaybeDocument*,
const Timestamp&) const {
VerifyKeyMatches(maybe_doc.get());

if (!precondition().IsValidFor(maybe_doc.get())) {
if (maybe_doc) {
return absl::make_unique<MaybeDocument>(maybe_doc->key(),
maybe_doc->version());
}
return nullptr;
}

SnapshotVersion version = GetPostMutationVersion(maybe_doc.get());
FieldValue new_data = PatchDocument(maybe_doc.get());
return absl::make_unique<Document>(std::move(new_data), key(), version,
/*has_local_mutations=*/true);
}

FieldValue PatchMutation::PatchDocument(const MaybeDocument* maybe_doc) const {
if (maybe_doc && maybe_doc->type() == MaybeDocument::Type::Document) {
return PatchObject(static_cast<const Document*>(maybe_doc)->data());
} else {
return PatchObject(FieldValue::FromMap({}));
}
}

FieldValue PatchMutation::PatchObject(FieldValue obj) const {
HARD_ASSERT(obj.type() == FieldValue::Type::Object);
for (const FieldPath& path : mask_) {
if (!path.empty()) {
absl::optional<FieldValue> new_value = value_.Get(path);
if (!new_value) {
obj = obj.Delete(path);
} else {
obj = obj.Set(path, *new_value);
}
}
}
return obj;
}

} // namespace model
} // namespace firestore
} // namespace firebase
36 changes: 36 additions & 0 deletions Firestore/core/src/firebase/firestore/model/mutation.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <memory>

#include "Firestore/core/src/firebase/firestore/model/document_key.h"
#include "Firestore/core/src/firebase/firestore/model/field_mask.h"
#include "Firestore/core/src/firebase/firestore/model/field_value.h"
#include "Firestore/core/src/firebase/firestore/model/maybe_document.h"
#include "Firestore/core/src/firebase/firestore/model/precondition.h"
Expand Down Expand Up @@ -141,6 +142,41 @@ class SetMutation : public Mutation {
const FieldValue value_;
};

/**
* A mutation that modifies fields of the document at the given key with the
* given values. The values are applied through a field mask:
*
* - When a field is in both the mask and the values, the corresponding field is
* updated.
* - When a field is in neither the mask nor the values, the corresponding field
* is unmodified.
* - When a field is in the mask but not in the values, the corresponding field
* is deleted.
* - When a field is not in the mask but is in the values, the values map is
* ignored.
*/
class PatchMutation : public Mutation {
public:
PatchMutation(DocumentKey&& key,
FieldValue&& value,
FieldMask&& mask,
Precondition&& precondition);

// TODO(rsgowman): ApplyToRemoteDocument()

std::shared_ptr<const MaybeDocument> ApplyToLocalView(
const std::shared_ptr<const MaybeDocument>& maybe_doc,
const MaybeDocument* base_doc,
const Timestamp& local_write_time) const override;

private:
FieldValue PatchDocument(const MaybeDocument* maybe_doc) const;
FieldValue PatchObject(FieldValue obj) const;

const FieldValue value_;
const FieldMask mask_;
};

} // namespace model
} // namespace firestore
} // namespace firebase
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ cc_test(
snapshot_version_test.cc
DEPENDS
firebase_firestore_model
firebase_firestore_testutil
)
105 changes: 102 additions & 3 deletions Firestore/core/test/firebase/firestore/model/mutation_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ namespace firebase {
namespace firestore {
namespace model {

using testutil::DeletedDoc;
using testutil::Doc;
using testutil::Field;
using testutil::PatchMutation;
using testutil::SetMutation;

TEST(Mutation, AppliesSetsToDocuments) {
Expand All @@ -41,10 +44,106 @@ TEST(Mutation, AppliesSetsToDocuments) {
std::shared_ptr<const MaybeDocument> set_doc =
set->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now());
ASSERT_TRUE(set_doc);
ASSERT_EQ(set_doc->type(), MaybeDocument::Type::Document);
EXPECT_EQ(*set_doc.get(), Doc("collection/key", 0,
{{"bar", FieldValue::FromString("bar-value")}},
/*has_local_mutations=*/true));
}

TEST(Mutation, AppliesPatchToDocuments) {
auto base_doc = std::make_shared<Document>(Doc(
"collection/key", 0,
{{"foo",
FieldValue::FromMap({{"bar", FieldValue::FromString("bar-value")}})},
{"baz", FieldValue::FromString("baz-value")}}));

std::unique_ptr<Mutation> patch = PatchMutation(
"collection/key", {{"foo.bar", FieldValue::FromString("new-bar-value")}});
std::shared_ptr<const MaybeDocument> local =
patch->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now());
ASSERT_TRUE(local);
EXPECT_EQ(
*local.get(),
Doc("collection/key", 0,
{{"foo", FieldValue::FromMap(
{{"bar", FieldValue::FromString("new-bar-value")}})},
{"baz", FieldValue::FromString("baz-value")}},
/*has_local_mutations=*/true));
}

TEST(Mutation, AppliesPatchWithMergeToDocuments) {
auto base_doc = std::make_shared<NoDocument>(DeletedDoc("collection/key", 0));

std::unique_ptr<Mutation> upsert = PatchMutation(
"collection/key", {{"foo.bar", FieldValue::FromString("new-bar-value")}},
{Field("foo.bar")});
std::shared_ptr<const MaybeDocument> new_doc =
upsert->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now());
ASSERT_TRUE(new_doc);
EXPECT_EQ(
*new_doc.get(),
Doc("collection/key", 0,
{{"foo", FieldValue::FromMap(
{{"bar", FieldValue::FromString("new-bar-value")}})}},
/*has_local_mutations=*/true));
}

TEST(Mutation, AppliesPatchToNullDocWithMergeToDocuments) {
std::shared_ptr<NoDocument> base_doc = nullptr;

std::unique_ptr<Mutation> upsert = PatchMutation(
"collection/key", {{"foo.bar", FieldValue::FromString("new-bar-value")}},
{Field("foo.bar")});
std::shared_ptr<const MaybeDocument> new_doc =
upsert->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now());
ASSERT_TRUE(new_doc);
EXPECT_EQ(
*new_doc.get(),
Doc("collection/key", 0,
{{"foo", FieldValue::FromMap(
{{"bar", FieldValue::FromString("new-bar-value")}})}},
/*has_local_mutations=*/true));
}

TEST(Mutation, DeletesValuesFromTheFieldMask) {
auto base_doc = std::make_shared<Document>(Doc(
"collection/key", 0,
{{"foo",
FieldValue::FromMap({{"bar", FieldValue::FromString("bar-value")},
{"baz", FieldValue::FromString("baz-value")}})}}));

std::unique_ptr<Mutation> patch =
PatchMutation("collection/key", {}, {Field("foo.bar")});

std::shared_ptr<const MaybeDocument> patch_doc =
patch->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now());
ASSERT_TRUE(patch_doc);
EXPECT_EQ(*patch_doc.get(),
Doc("collection/key", 0,
{{"foo", FieldValue::FromMap(
{{"baz", FieldValue::FromString("baz-value")}})}},
/*has_local_mutations=*/true));
}

TEST(Mutation, PatchesPrimitiveValue) {
auto base_doc = std::make_shared<Document>(
Doc("collection/key", 0,
{{"foo", FieldValue::FromString("foo-value")},
{"baz", FieldValue::FromString("baz-value")}}));

std::unique_ptr<Mutation> patch = PatchMutation(
"collection/key", {{"foo.bar", FieldValue::FromString("new-bar-value")}});

std::shared_ptr<const MaybeDocument> patched_doc =
patch->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now());
ASSERT_TRUE(patched_doc);
EXPECT_EQ(
Doc("collection/key", 0, {{"bar", FieldValue::FromString("bar-value")}},
/*has_local_mutations=*/true),
*set_doc.get());
*patched_doc.get(),
Doc("collection/key", 0,
{{"foo", FieldValue::FromMap(
{{"bar", FieldValue::FromString("new-bar-value")}})},
{"baz", FieldValue::FromString("baz-value")}},
/*has_local_mutations=*/true));
}

} // namespace model
Expand Down
25 changes: 24 additions & 1 deletion Firestore/core/test/firebase/firestore/testutil/testutil.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,30 @@ namespace firebase {
namespace firestore {
namespace testutil {

void dummy() {
std::unique_ptr<model::PatchMutation> PatchMutation(
absl::string_view path,
const model::ObjectValue::Map& values,
const std::vector<model::FieldPath>* update_mask) {
model::FieldValue object_value = model::FieldValue::FromMap({});
std::vector<model::FieldPath> object_mask;

for (const auto& kv : values) {
model::FieldPath field_path = Field(kv.first);
object_mask.push_back(field_path);
if (kv.second.string_value() != kDeleteSentinel) {
object_value = object_value.Set(field_path, kv.second);
}
}

bool merge = update_mask != nullptr;

// We sort the field_mask_paths to make the order deterministic in tests.
std::sort(object_mask.begin(), object_mask.end());

return absl::make_unique<model::PatchMutation>(
Key(path), std::move(object_value),
model::FieldMask(merge ? *update_mask : object_mask),
merge ? model::Precondition::None() : model::Precondition::Exists(true));
}

} // namespace testutil
Expand Down
23 changes: 19 additions & 4 deletions Firestore/core/test/firebase/firestore/testutil/testutil.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#ifndef FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_TESTUTIL_TESTUTIL_H_
#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_TESTUTIL_TESTUTIL_H_

#include <algorithm>
#include <chrono> // NOLINT(build/c++11)
#include <cstdint>
#include <memory>
Expand All @@ -43,6 +44,12 @@ namespace firebase {
namespace firestore {
namespace testutil {

/**
* A string sentinel that can be used with PatchMutation() to mark a field for
* deletion.
*/
constexpr const char* kDeleteSentinel = "<DELETE>";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this already defined somewhere not specific to test? Or actually you want to test the sentinel is the same?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it only seems to appear in the testutil class (at least for android, objc, and now c++)


// Below are convenience methods for creating instances for tests.

inline model::DocumentKey Key(absl::string_view path) {
Expand Down Expand Up @@ -131,6 +138,18 @@ inline std::unique_ptr<model::SetMutation> SetMutation(
model::Precondition::None());
}

std::unique_ptr<model::PatchMutation> PatchMutation(
absl::string_view path,
const model::ObjectValue::Map& values = {},
const std::vector<model::FieldPath>* update_mask = nullptr);

inline std::unique_ptr<model::PatchMutation> PatchMutation(
absl::string_view path,
const model::ObjectValue::Map& values,
const std::vector<model::FieldPath>& update_mask) {
return PatchMutation(path, values, &update_mask);
}

inline std::vector<uint8_t> ResumeToken(int64_t snapshot_version) {
if (snapshot_version == 0) {
// TODO(rsgowman): The other platforms return null here, though I'm not sure
Expand All @@ -145,10 +164,6 @@ inline std::vector<uint8_t> ResumeToken(int64_t snapshot_version) {
return {snapshot_string.begin(), snapshot_string.end()};
}

// Add a non-inline function to make this library buildable.
// TODO(zxu123): remove once there is non-inline function.
void dummy();

} // namespace testutil
} // namespace firestore
} // namespace firebase
Expand Down