Skip to content

Commit 53159e9

Browse files
committed
Firestore: Implement firestore merge as a boolean
1 parent c2f2646 commit 53159e9

File tree

12 files changed

+58
-185
lines changed

12 files changed

+58
-185
lines changed

firestore/google/cloud/firestore.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from google.cloud.firestore_v1beta1 import ExistsOption
2626
from google.cloud.firestore_v1beta1 import GeoPoint
2727
from google.cloud.firestore_v1beta1 import LastUpdateOption
28-
from google.cloud.firestore_v1beta1 import MergeOption
2928
from google.cloud.firestore_v1beta1 import Query
3029
from google.cloud.firestore_v1beta1 import ReadAfterWriteError
3130
from google.cloud.firestore_v1beta1 import SERVER_TIMESTAMP
@@ -47,7 +46,6 @@
4746
'ExistsOption',
4847
'GeoPoint',
4948
'LastUpdateOption',
50-
'MergeOption',
5149
'Query',
5250
'ReadAfterWriteError',
5351
'SERVER_TIMESTAMP',

firestore/google/cloud/firestore_v1beta1/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from google.cloud.firestore_v1beta1.client import Client
2525
from google.cloud.firestore_v1beta1.client import ExistsOption
2626
from google.cloud.firestore_v1beta1.client import LastUpdateOption
27-
from google.cloud.firestore_v1beta1.client import MergeOption
2827
from google.cloud.firestore_v1beta1.client import WriteOption
2928
from google.cloud.firestore_v1beta1.collection import CollectionReference
3029
from google.cloud.firestore_v1beta1.constants import DELETE_FIELD
@@ -48,7 +47,6 @@
4847
'ExistsOption',
4948
'GeoPoint',
5049
'LastUpdateOption',
51-
'MergeOption',
5250
'Query',
5351
'ReadAfterWriteError',
5452
'SERVER_TIMESTAMP',

firestore/google/cloud/firestore_v1beta1/_helpers.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -517,18 +517,18 @@ def encode_dict(values_dict):
517517
}
518518

519519

520-
def extract_field_paths(actual_data):
521-
"""Extract field paths from actual data
520+
def extract_field_paths(document_data):
521+
"""Extract field paths from document data
522522
523523
Args:
524-
actual_data (dict): The dictionary of the actual set data.
524+
document_data (dict): The dictionary of the actual set data.
525525
526526
Returns:
527527
List[~.firestore_v1beta1._helpers.FieldPath]:
528528
A list of `FieldPath` instances from the actual data.
529529
"""
530530
field_paths = []
531-
for field_name, value in six.iteritems(actual_data):
531+
for field_name, value in six.iteritems(document_data):
532532
if isinstance(value, dict):
533533
sub_field_paths = extract_field_paths(value)
534534
for sub_path in sub_field_paths:
@@ -897,7 +897,7 @@ def get_transform_pb(document_path, transform_paths):
897897
)
898898

899899

900-
def pbs_for_set(document_path, document_data, option):
900+
def pbs_for_set(document_path, document_data, merge=False, exists=None):
901901
"""Make ``Write`` protobufs for ``set()`` methods.
902902
903903
Args:
@@ -920,9 +920,15 @@ def pbs_for_set(document_path, document_data, option):
920920
fields=encode_dict(actual_data),
921921
),
922922
)
923-
if option is not None:
923+
if exists is not None:
924+
update_pb.current_document.CopyFrom(
925+
common_pb2.Precondition(exists=exists))
926+
927+
if merge:
924928
field_paths = extract_field_paths(document_data)
925-
option.modify_write(update_pb, field_paths=field_paths)
929+
field_paths = canonicalize_field_paths(field_paths)
930+
mask = common_pb2.DocumentMask(field_paths=sorted(field_paths))
931+
update_pb.update_mask.CopyFrom(mask)
926932

927933
write_pbs = [update_pb]
928934
if transform_paths:

firestore/google/cloud/firestore_v1beta1/batch.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,11 @@ def create(self, reference, document_data):
5757
document_data (dict): Property names and values to use for
5858
creating a document.
5959
"""
60-
option = self._client.write_option(exists=False)
61-
self.set(reference, document_data, option=option)
60+
write_pbs = _helpers.pbs_for_set(
61+
reference._document_path, document_data, merge=False, exists=False)
62+
self._add_write_pbs(write_pbs)
6263

63-
def set(self, reference, document_data, option=None):
64+
def set(self, reference, document_data, merge=False):
6465
"""Add a "change" to replace a document.
6566
6667
See
@@ -69,16 +70,16 @@ def set(self, reference, document_data, option=None):
6970
applied.
7071
7172
Args:
72-
reference (~.firestore_v1beta1.document.DocumentReference): A
73-
document reference that will have values set in this batch.
74-
document_data (dict): Property names and values to use for
75-
replacing a document.
76-
option (Optional[~.firestore_v1beta1.client.WriteOption]): A
77-
write option to make assertions / preconditions on the server
78-
state of the document before applying changes.
73+
reference (~.firestore_v1beta1.document.DocumentReference):
74+
A document reference that will have values set in this batch.
75+
document_data (dict):
76+
Property names and values to use for replacing a document.
77+
merge (Optional[bool]):
78+
If True, apply merging instead of overwriting the state
79+
of the document.
7980
"""
8081
write_pbs = _helpers.pbs_for_set(
81-
reference._document_path, document_data, option)
82+
reference._document_path, document_data, merge=merge)
8283
self._add_write_pbs(write_pbs)
8384

8485
def update(self, reference, field_updates, option=None):

firestore/google/cloud/firestore_v1beta1/client.py

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,13 @@
3333
from google.cloud.firestore_v1beta1.document import DocumentReference
3434
from google.cloud.firestore_v1beta1.document import DocumentSnapshot
3535
from google.cloud.firestore_v1beta1.gapic import firestore_client
36-
from google.cloud.firestore_v1beta1.proto import common_pb2
3736
from google.cloud.firestore_v1beta1.transaction import Transaction
3837

3938

4039
DEFAULT_DATABASE = '(default)'
4140
"""str: The default database used in a :class:`~.firestore.client.Client`."""
4241
_BAD_OPTION_ERR = (
43-
'Exactly one of ``last_update_time``, ``merge`` or ``exists`` '
42+
'Exactly one of ``last_update_time``, ``exists`` '
4443
'must be provided.')
4544
_BAD_DOC_TEMPLATE = (
4645
'Document {!r} appeared in response but was not present among references')
@@ -261,9 +260,6 @@ def write_option(**kwargs):
261260
protobuf or directly.
262261
* ``exists`` (:class:`bool`): Indicates if the document being modified
263262
should already exist.
264-
* ``merge`` (Any):
265-
Indicates if the document should be merged.
266-
**Note**: argument is ignored
267263
268264
Providing no argument would make the option have no effect (so
269265
it is not allowed). Providing multiple would be an apparent
@@ -287,8 +283,6 @@ def write_option(**kwargs):
287283
return LastUpdateOption(value)
288284
elif name == 'exists':
289285
return ExistsOption(value)
290-
elif name == 'merge':
291-
return MergeOption()
292286
else:
293287
extra = '{!r} was provided'.format(name)
294288
raise TypeError(_BAD_OPTION_ERR, extra)
@@ -424,32 +418,6 @@ def modify_write(self, write_pb, **unused_kwargs):
424418
write_pb.current_document.CopyFrom(current_doc)
425419

426420

427-
class MergeOption(WriteOption):
428-
"""Option used to merge on a write operation.
429-
430-
This will typically be created by
431-
:meth:`~.firestore_v1beta1.client.Client.write_option`.
432-
"""
433-
def modify_write(
434-
self, write_pb, field_paths=None, **unused_kwargs):
435-
"""Modify a ``Write`` protobuf based on the state of this write option.
436-
437-
Args:
438-
write_pb (google.cloud.firestore_v1beta1.types.Write): A
439-
``Write`` protobuf instance to be modified with a precondition
440-
determined by the state of this option.
441-
field_paths (Sequence[str]):
442-
The actual field names to use for replacing a document.
443-
unused_kwargs (Dict[str, Any]): Keyword arguments accepted by
444-
other subclasses that are unused here.
445-
"""
446-
if field_paths is None:
447-
field_paths = []
448-
field_paths = _helpers.canonicalize_field_paths(field_paths)
449-
mask = common_pb2.DocumentMask(field_paths=sorted(field_paths))
450-
write_pb.update_mask.CopyFrom(mask)
451-
452-
453421
class ExistsOption(WriteOption):
454422
"""Option used to assert existence on a write operation.
455423

firestore/google/cloud/firestore_v1beta1/document.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def create(self, document_data):
192192
write_results = batch.commit()
193193
return _first_write_result(write_results)
194194

195-
def set(self, document_data, option=None):
195+
def set(self, document_data, merge=False):
196196
"""Replace the current document in the Firestore database.
197197
198198
A write ``option`` can be specified to indicate preconditions of
@@ -219,7 +219,7 @@ def set(self, document_data, option=None):
219219
result contains an ``update_time`` field.
220220
"""
221221
batch = self._client.batch()
222-
batch.set(self, document_data, option=option)
222+
batch.set(self, document_data, merge=merge)
223223
write_results = batch.commit()
224224
return _first_write_result(write_results)
225225

firestore/tests/system.py

Lines changed: 10 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,9 @@ def test_document_set(client, cleanup):
143143
snapshot = document.get()
144144
assert snapshot.to_dict() is None
145145

146-
# 1. Use ``set()`` to create the document (using an option).
146+
# 1. Use ``create()`` to create the document (using an option).
147147
data1 = {'foo': 88}
148-
option1 = client.write_option(exists=False)
149-
write_result1 = document.set(data1, option=option1)
148+
write_result1 = document.create(data1)
150149
snapshot1 = document.get()
151150
assert snapshot1.to_dict() == data1
152151
# Make sure the update is what created the document.
@@ -162,30 +161,6 @@ def test_document_set(client, cleanup):
162161
assert snapshot2.create_time == snapshot1.create_time
163162
assert snapshot2.update_time == write_result2.update_time
164163

165-
# 3. Call ``set()`` with a valid "last timestamp" option.
166-
data3 = {'skates': 88}
167-
option3 = client.write_option(last_update_time=snapshot2.update_time)
168-
write_result3 = document.set(data3, option=option3)
169-
snapshot3 = document.get()
170-
assert snapshot3.to_dict() == data3
171-
# Make sure the create time hasn't changed.
172-
assert snapshot3.create_time == snapshot1.create_time
173-
assert snapshot3.update_time == write_result3.update_time
174-
175-
# 4. Call ``set()`` with invalid (in the past) "last timestamp" option.
176-
assert_timestamp_less(option3._last_update_time, snapshot3.update_time)
177-
with pytest.raises(FailedPrecondition):
178-
document.set({'bad': 'time-past'}, option=option3)
179-
180-
# 5. Call ``set()`` with invalid (in the future) "last timestamp" option.
181-
timestamp_pb = timestamp_pb2.Timestamp(
182-
seconds=snapshot3.update_time.nanos + 120,
183-
nanos=snapshot3.update_time.nanos,
184-
)
185-
option5 = client.write_option(last_update_time=timestamp_pb)
186-
with pytest.raises(FailedPrecondition):
187-
document.set({'bad': 'time-future'}, option=option5)
188-
189164

190165
def test_document_integer_field(client, cleanup):
191166
document_id = 'for-set' + unique_resource_id('-')
@@ -201,8 +176,7 @@ def test_document_integer_field(client, cleanup):
201176
'7g': '8h',
202177
'cd': '0j'}
203178
}
204-
option1 = client.write_option(exists=False)
205-
document.set(data1, option=option1)
179+
document.create(data1)
206180

207181
data2 = {'1a.ab': '4d', '6f.7g': '9h'}
208182
option2 = client.write_option(exists=True)
@@ -225,30 +199,24 @@ def test_document_set_merge(client, cleanup):
225199
# Add to clean-up before API request (in case ``set()`` fails).
226200
cleanup(document)
227201

228-
# 0. Make sure the document doesn't exist yet using an option.
229-
option0 = client.write_option(exists=True)
230-
with pytest.raises(NotFound) as exc_info:
231-
document.set({'no': 'way'}, option=option0)
232-
233-
assert exc_info.value.message.startswith(MISSING_DOCUMENT)
234-
assert document_id in exc_info.value.message
202+
# 0. Make sure the document doesn't exist yet
203+
snapshot = document.get()
204+
assert not snapshot.exists
235205

236206
# 1. Use ``set()`` to create the document (using an option).
237207
data1 = {'name': 'Sam',
238208
'address': {'city': 'SF',
239209
'state': 'CA'}}
240-
option1 = client.write_option(exists=False)
241-
write_result1 = document.set(data1, option=option1)
210+
write_result1 = document.create(data1)
242211
snapshot1 = document.get()
243212
assert snapshot1.to_dict() == data1
244213
# Make sure the update is what created the document.
245214
assert snapshot1.create_time == snapshot1.update_time
246215
assert snapshot1.update_time == write_result1.update_time
247216

248-
# 2. Call ``set()`` again to overwrite (no option).
217+
# 2. Call ``set()`` to merge
249218
data2 = {'address': {'city': 'LA'}}
250-
option2 = client.write_option(merge=True)
251-
write_result2 = document.set(data2, option=option2)
219+
write_result2 = document.set(data2, merge=True)
252220
snapshot2 = document.get()
253221
assert snapshot2.to_dict() == {'name': 'Sam',
254222
'address': {'city': 'LA',
@@ -333,7 +301,7 @@ def test_update_document(client, cleanup):
333301
)
334302
option6 = client.write_option(last_update_time=timestamp_pb)
335303
with pytest.raises(FailedPrecondition) as exc_info:
336-
document.set({'bad': 'time-future'}, option=option6)
304+
document.update({'bad': 'time-future'}, option=option6)
337305

338306

339307
def check_snapshot(snapshot, document, data, write_result):

firestore/tests/unit/test__helpers.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,8 +1397,7 @@ def _call_fut(document_path, document_data, option):
13971397

13981398
return pbs_for_set(document_path, document_data, option)
13991399

1400-
def _helper(self, option=None, do_transform=False, **write_kwargs):
1401-
from google.cloud.firestore_v1beta1.client import MergeOption
1400+
def _helper(self, merge=False, do_transform=False, **write_kwargs):
14021401
from google.cloud.firestore_v1beta1.constants import SERVER_TIMESTAMP
14031402
from google.cloud.firestore_v1beta1.gapic import enums
14041403
from google.cloud.firestore_v1beta1.proto import common_pb2
@@ -1420,7 +1419,7 @@ def _helper(self, option=None, do_transform=False, **write_kwargs):
14201419
if do_transform:
14211420
document_data[field_name3] = SERVER_TIMESTAMP
14221421

1423-
write_pbs = self._call_fut(document_path, document_data, option)
1422+
write_pbs = self._call_fut(document_path, document_data, merge)
14241423

14251424
expected_update_pb = write_pb2.Write(
14261425
update=document_pb2.Document(
@@ -1434,7 +1433,7 @@ def _helper(self, option=None, do_transform=False, **write_kwargs):
14341433
)
14351434
expected_pbs = [expected_update_pb]
14361435

1437-
if isinstance(option, MergeOption):
1436+
if merge:
14381437
field_paths = [field_name1, field_name2]
14391438
mask = common_pb2.DocumentMask(field_paths=sorted(field_paths))
14401439
expected_pbs[0].update_mask.CopyFrom(mask)
@@ -1459,19 +1458,8 @@ def _helper(self, option=None, do_transform=False, **write_kwargs):
14591458
def test_without_option(self):
14601459
self._helper()
14611460

1462-
def test_with_exists_option(self):
1463-
from google.cloud.firestore_v1beta1.proto import common_pb2
1464-
from google.cloud.firestore_v1beta1.client import ExistsOption
1465-
1466-
option = ExistsOption(True)
1467-
precondition = common_pb2.Precondition(exists=True)
1468-
self._helper(option=option, current_document=precondition)
1469-
14701461
def test_with_merge_option(self):
1471-
from google.cloud.firestore_v1beta1.client import MergeOption
1472-
1473-
option = MergeOption()
1474-
self._helper(option=option)
1462+
self._helper(merge=True)
14751463

14761464
def test_update_and_transform(self):
14771465
self._helper(do_transform=True)

firestore/tests/unit/test_batch.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ def test_set(self):
9393
def test_set_merge(self):
9494
from google.cloud.firestore_v1beta1.proto import document_pb2
9595
from google.cloud.firestore_v1beta1.proto import write_pb2
96-
from google.cloud.firestore_v1beta1.client import MergeOption
9796

9897
client = _make_client()
9998
batch = self._make_one(client)
@@ -103,8 +102,7 @@ def test_set_merge(self):
103102
field = 'zapzap'
104103
value = u'meadows and flowers'
105104
document_data = {field: value}
106-
option = MergeOption()
107-
ret_val = batch.set(reference, document_data, option)
105+
ret_val = batch.set(reference, document_data, merge=True)
108106
self.assertIsNone(ret_val)
109107
new_write_pb = write_pb2.Write(
110108
update=document_pb2.Document(

0 commit comments

Comments
 (0)