Skip to content

Commit f66b5b5

Browse files
authored
Implement PatchMutation::ApplyToLocalView (#1973)
1 parent 0540361 commit f66b5b5

File tree

6 files changed

+235
-8
lines changed

6 files changed

+235
-8
lines changed

Firestore/core/src/firebase/firestore/model/mutation.cc

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <utility>
2020

2121
#include "Firestore/core/src/firebase/firestore/model/document.h"
22+
#include "Firestore/core/src/firebase/firestore/model/field_path.h"
2223
#include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
2324

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

71+
PatchMutation::PatchMutation(DocumentKey&& key,
72+
FieldValue&& value,
73+
FieldMask&& mask,
74+
Precondition&& precondition)
75+
: Mutation(std::move(key), std::move(precondition)),
76+
value_(std::move(value)),
77+
mask_(std::move(mask)) {
78+
}
79+
80+
std::shared_ptr<const MaybeDocument> PatchMutation::ApplyToLocalView(
81+
const std::shared_ptr<const MaybeDocument>& maybe_doc,
82+
const MaybeDocument*,
83+
const Timestamp&) const {
84+
VerifyKeyMatches(maybe_doc.get());
85+
86+
if (!precondition().IsValidFor(maybe_doc.get())) {
87+
if (maybe_doc) {
88+
return absl::make_unique<MaybeDocument>(maybe_doc->key(),
89+
maybe_doc->version());
90+
}
91+
return nullptr;
92+
}
93+
94+
SnapshotVersion version = GetPostMutationVersion(maybe_doc.get());
95+
FieldValue new_data = PatchDocument(maybe_doc.get());
96+
return absl::make_unique<Document>(std::move(new_data), key(), version,
97+
/*has_local_mutations=*/true);
98+
}
99+
100+
FieldValue PatchMutation::PatchDocument(const MaybeDocument* maybe_doc) const {
101+
if (maybe_doc && maybe_doc->type() == MaybeDocument::Type::Document) {
102+
return PatchObject(static_cast<const Document*>(maybe_doc)->data());
103+
} else {
104+
return PatchObject(FieldValue::FromMap({}));
105+
}
106+
}
107+
108+
FieldValue PatchMutation::PatchObject(FieldValue obj) const {
109+
HARD_ASSERT(obj.type() == FieldValue::Type::Object);
110+
for (const FieldPath& path : mask_) {
111+
if (!path.empty()) {
112+
absl::optional<FieldValue> new_value = value_.Get(path);
113+
if (!new_value) {
114+
obj = obj.Delete(path);
115+
} else {
116+
obj = obj.Set(path, *new_value);
117+
}
118+
}
119+
}
120+
return obj;
121+
}
122+
70123
} // namespace model
71124
} // namespace firestore
72125
} // namespace firebase

Firestore/core/src/firebase/firestore/model/mutation.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <memory>
2121

2222
#include "Firestore/core/src/firebase/firestore/model/document_key.h"
23+
#include "Firestore/core/src/firebase/firestore/model/field_mask.h"
2324
#include "Firestore/core/src/firebase/firestore/model/field_value.h"
2425
#include "Firestore/core/src/firebase/firestore/model/maybe_document.h"
2526
#include "Firestore/core/src/firebase/firestore/model/precondition.h"
@@ -141,6 +142,41 @@ class SetMutation : public Mutation {
141142
const FieldValue value_;
142143
};
143144

145+
/**
146+
* A mutation that modifies fields of the document at the given key with the
147+
* given values. The values are applied through a field mask:
148+
*
149+
* - When a field is in both the mask and the values, the corresponding field is
150+
* updated.
151+
* - When a field is in neither the mask nor the values, the corresponding field
152+
* is unmodified.
153+
* - When a field is in the mask but not in the values, the corresponding field
154+
* is deleted.
155+
* - When a field is not in the mask but is in the values, the values map is
156+
* ignored.
157+
*/
158+
class PatchMutation : public Mutation {
159+
public:
160+
PatchMutation(DocumentKey&& key,
161+
FieldValue&& value,
162+
FieldMask&& mask,
163+
Precondition&& precondition);
164+
165+
// TODO(rsgowman): ApplyToRemoteDocument()
166+
167+
std::shared_ptr<const MaybeDocument> ApplyToLocalView(
168+
const std::shared_ptr<const MaybeDocument>& maybe_doc,
169+
const MaybeDocument* base_doc,
170+
const Timestamp& local_write_time) const override;
171+
172+
private:
173+
FieldValue PatchDocument(const MaybeDocument* maybe_doc) const;
174+
FieldValue PatchObject(FieldValue obj) const;
175+
176+
const FieldValue value_;
177+
const FieldMask mask_;
178+
};
179+
144180
} // namespace model
145181
} // namespace firestore
146182
} // namespace firebase

Firestore/core/test/firebase/firestore/model/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ cc_test(
2929
snapshot_version_test.cc
3030
DEPENDS
3131
firebase_firestore_model
32+
firebase_firestore_testutil
3233
)

Firestore/core/test/firebase/firestore/model/mutation_test.cc

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ namespace firebase {
2727
namespace firestore {
2828
namespace model {
2929

30+
using testutil::DeletedDoc;
3031
using testutil::Doc;
32+
using testutil::Field;
33+
using testutil::PatchMutation;
3134
using testutil::SetMutation;
3235

3336
TEST(Mutation, AppliesSetsToDocuments) {
@@ -41,10 +44,106 @@ TEST(Mutation, AppliesSetsToDocuments) {
4144
std::shared_ptr<const MaybeDocument> set_doc =
4245
set->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now());
4346
ASSERT_TRUE(set_doc);
47+
ASSERT_EQ(set_doc->type(), MaybeDocument::Type::Document);
48+
EXPECT_EQ(*set_doc.get(), Doc("collection/key", 0,
49+
{{"bar", FieldValue::FromString("bar-value")}},
50+
/*has_local_mutations=*/true));
51+
}
52+
53+
TEST(Mutation, AppliesPatchToDocuments) {
54+
auto base_doc = std::make_shared<Document>(Doc(
55+
"collection/key", 0,
56+
{{"foo",
57+
FieldValue::FromMap({{"bar", FieldValue::FromString("bar-value")}})},
58+
{"baz", FieldValue::FromString("baz-value")}}));
59+
60+
std::unique_ptr<Mutation> patch = PatchMutation(
61+
"collection/key", {{"foo.bar", FieldValue::FromString("new-bar-value")}});
62+
std::shared_ptr<const MaybeDocument> local =
63+
patch->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now());
64+
ASSERT_TRUE(local);
65+
EXPECT_EQ(
66+
*local.get(),
67+
Doc("collection/key", 0,
68+
{{"foo", FieldValue::FromMap(
69+
{{"bar", FieldValue::FromString("new-bar-value")}})},
70+
{"baz", FieldValue::FromString("baz-value")}},
71+
/*has_local_mutations=*/true));
72+
}
73+
74+
TEST(Mutation, AppliesPatchWithMergeToDocuments) {
75+
auto base_doc = std::make_shared<NoDocument>(DeletedDoc("collection/key", 0));
76+
77+
std::unique_ptr<Mutation> upsert = PatchMutation(
78+
"collection/key", {{"foo.bar", FieldValue::FromString("new-bar-value")}},
79+
{Field("foo.bar")});
80+
std::shared_ptr<const MaybeDocument> new_doc =
81+
upsert->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now());
82+
ASSERT_TRUE(new_doc);
83+
EXPECT_EQ(
84+
*new_doc.get(),
85+
Doc("collection/key", 0,
86+
{{"foo", FieldValue::FromMap(
87+
{{"bar", FieldValue::FromString("new-bar-value")}})}},
88+
/*has_local_mutations=*/true));
89+
}
90+
91+
TEST(Mutation, AppliesPatchToNullDocWithMergeToDocuments) {
92+
std::shared_ptr<NoDocument> base_doc = nullptr;
93+
94+
std::unique_ptr<Mutation> upsert = PatchMutation(
95+
"collection/key", {{"foo.bar", FieldValue::FromString("new-bar-value")}},
96+
{Field("foo.bar")});
97+
std::shared_ptr<const MaybeDocument> new_doc =
98+
upsert->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now());
99+
ASSERT_TRUE(new_doc);
100+
EXPECT_EQ(
101+
*new_doc.get(),
102+
Doc("collection/key", 0,
103+
{{"foo", FieldValue::FromMap(
104+
{{"bar", FieldValue::FromString("new-bar-value")}})}},
105+
/*has_local_mutations=*/true));
106+
}
107+
108+
TEST(Mutation, DeletesValuesFromTheFieldMask) {
109+
auto base_doc = std::make_shared<Document>(Doc(
110+
"collection/key", 0,
111+
{{"foo",
112+
FieldValue::FromMap({{"bar", FieldValue::FromString("bar-value")},
113+
{"baz", FieldValue::FromString("baz-value")}})}}));
114+
115+
std::unique_ptr<Mutation> patch =
116+
PatchMutation("collection/key", {}, {Field("foo.bar")});
117+
118+
std::shared_ptr<const MaybeDocument> patch_doc =
119+
patch->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now());
120+
ASSERT_TRUE(patch_doc);
121+
EXPECT_EQ(*patch_doc.get(),
122+
Doc("collection/key", 0,
123+
{{"foo", FieldValue::FromMap(
124+
{{"baz", FieldValue::FromString("baz-value")}})}},
125+
/*has_local_mutations=*/true));
126+
}
127+
128+
TEST(Mutation, PatchesPrimitiveValue) {
129+
auto base_doc = std::make_shared<Document>(
130+
Doc("collection/key", 0,
131+
{{"foo", FieldValue::FromString("foo-value")},
132+
{"baz", FieldValue::FromString("baz-value")}}));
133+
134+
std::unique_ptr<Mutation> patch = PatchMutation(
135+
"collection/key", {{"foo.bar", FieldValue::FromString("new-bar-value")}});
136+
137+
std::shared_ptr<const MaybeDocument> patched_doc =
138+
patch->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now());
139+
ASSERT_TRUE(patched_doc);
44140
EXPECT_EQ(
45-
Doc("collection/key", 0, {{"bar", FieldValue::FromString("bar-value")}},
46-
/*has_local_mutations=*/true),
47-
*set_doc.get());
141+
*patched_doc.get(),
142+
Doc("collection/key", 0,
143+
{{"foo", FieldValue::FromMap(
144+
{{"bar", FieldValue::FromString("new-bar-value")}})},
145+
{"baz", FieldValue::FromString("baz-value")}},
146+
/*has_local_mutations=*/true));
48147
}
49148

50149
} // namespace model

Firestore/core/test/firebase/firestore/testutil/testutil.cc

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,30 @@ namespace firebase {
2020
namespace firestore {
2121
namespace testutil {
2222

23-
void dummy() {
23+
std::unique_ptr<model::PatchMutation> PatchMutation(
24+
absl::string_view path,
25+
const model::ObjectValue::Map& values,
26+
const std::vector<model::FieldPath>* update_mask) {
27+
model::FieldValue object_value = model::FieldValue::FromMap({});
28+
std::vector<model::FieldPath> object_mask;
29+
30+
for (const auto& kv : values) {
31+
model::FieldPath field_path = Field(kv.first);
32+
object_mask.push_back(field_path);
33+
if (kv.second.string_value() != kDeleteSentinel) {
34+
object_value = object_value.Set(field_path, kv.second);
35+
}
36+
}
37+
38+
bool merge = update_mask != nullptr;
39+
40+
// We sort the field_mask_paths to make the order deterministic in tests.
41+
std::sort(object_mask.begin(), object_mask.end());
42+
43+
return absl::make_unique<model::PatchMutation>(
44+
Key(path), std::move(object_value),
45+
model::FieldMask(merge ? *update_mask : object_mask),
46+
merge ? model::Precondition::None() : model::Precondition::Exists(true));
2447
}
2548

2649
} // namespace testutil

Firestore/core/test/firebase/firestore/testutil/testutil.h

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#ifndef FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_TESTUTIL_TESTUTIL_H_
1818
#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_TESTUTIL_TESTUTIL_H_
1919

20+
#include <algorithm>
2021
#include <chrono> // NOLINT(build/c++11)
2122
#include <cstdint>
2223
#include <memory>
@@ -43,6 +44,12 @@ namespace firebase {
4344
namespace firestore {
4445
namespace testutil {
4546

47+
/**
48+
* A string sentinel that can be used with PatchMutation() to mark a field for
49+
* deletion.
50+
*/
51+
constexpr const char* kDeleteSentinel = "<DELETE>";
52+
4653
// Below are convenience methods for creating instances for tests.
4754

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

141+
std::unique_ptr<model::PatchMutation> PatchMutation(
142+
absl::string_view path,
143+
const model::ObjectValue::Map& values = {},
144+
const std::vector<model::FieldPath>* update_mask = nullptr);
145+
146+
inline std::unique_ptr<model::PatchMutation> PatchMutation(
147+
absl::string_view path,
148+
const model::ObjectValue::Map& values,
149+
const std::vector<model::FieldPath>& update_mask) {
150+
return PatchMutation(path, values, &update_mask);
151+
}
152+
134153
inline std::vector<uint8_t> ResumeToken(int64_t snapshot_version) {
135154
if (snapshot_version == 0) {
136155
// TODO(rsgowman): The other platforms return null here, though I'm not sure
@@ -145,10 +164,6 @@ inline std::vector<uint8_t> ResumeToken(int64_t snapshot_version) {
145164
return {snapshot_string.begin(), snapshot_string.end()};
146165
}
147166

148-
// Add a non-inline function to make this library buildable.
149-
// TODO(zxu123): remove once there is non-inline function.
150-
void dummy();
151-
152167
} // namespace testutil
153168
} // namespace firestore
154169
} // namespace firebase

0 commit comments

Comments
 (0)