Skip to content

Commit 1602062

Browse files
Protobuf-backed FieldValues
1 parent e4b4850 commit 1602062

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+865
-1737
lines changed

firebase-firestore/ktx/src/test/kotlin/com/google/firebase/firestore/ktx/FirestoreTests.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ class QuerySnapshotTests {
171171
val qs = TestUtil.querySnapshot(
172172
"rooms",
173173
mapOf(),
174-
mapOf("id" to ObjectValue.fromMap(mapOf("a" to IntegerValue.valueOf(1), "b" to IntegerValue.valueOf(2)))),
174+
mapOf("id" to ObjectValue.fromMap(mapOf("a" to IntegerValue.valueOf(1).proto, "b" to IntegerValue.valueOf(2).proto))),
175175
false,
176176
false)
177177

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -461,44 +461,49 @@ public void queriesCannotBeSortedByAnUncommittedServerTimestamp() {
461461
TaskCompletionSource<Void> offlineCallbackDone = new TaskCompletionSource<>();
462462
TaskCompletionSource<Void> onlineCallbackDone = new TaskCompletionSource<>();
463463

464-
collection.addSnapshotListener(
465-
(snapshot, error) -> {
466-
assertNotNull(snapshot);
467-
468-
// Skip the initial empty snapshot.
469-
if (snapshot.isEmpty()) return;
470-
471-
assertThat(snapshot.getDocuments()).hasSize(1);
472-
DocumentSnapshot docSnap = snapshot.getDocuments().get(0);
473-
474-
if (snapshot.getMetadata().hasPendingWrites()) {
475-
// Offline snapshot. Since the server timestamp is uncommitted, we shouldn't be able to
476-
// query by it.
477-
assertThrows(
478-
IllegalArgumentException.class,
479-
() ->
480-
collection
481-
.orderBy("timestamp")
482-
.endAt(docSnap)
483-
.addSnapshotListener((snapshot2, error2) -> {}));
484-
offlineCallbackDone.setResult(null);
485-
} else {
486-
// Online snapshot. Since the server timestamp is committed, we should be able to query
487-
// by it.
488-
collection
489-
.orderBy("timestamp")
490-
.endAt(docSnap)
491-
.addSnapshotListener((snapshot2, error2) -> {});
492-
onlineCallbackDone.setResult(null);
493-
}
494-
});
464+
ListenerRegistration listenerRegistration =
465+
collection.addSnapshotListener(
466+
(snapshot, error) -> {
467+
assertNotNull(snapshot);
468+
469+
// Skip the initial empty snapshot.
470+
if (snapshot.isEmpty()) return;
471+
472+
assertThat(snapshot.getDocuments()).hasSize(1);
473+
DocumentSnapshot docSnap = snapshot.getDocuments().get(0);
474+
475+
if (snapshot.getMetadata().hasPendingWrites()) {
476+
// Offline snapshot. Since the server timestamp is uncommitted, we shouldn't be able
477+
// to query by it.
478+
assertThrows(
479+
IllegalArgumentException.class,
480+
() ->
481+
collection
482+
.orderBy("timestamp")
483+
.endAt(docSnap)
484+
.addSnapshotListener((snapshot2, error2) -> {}));
485+
// Use `trySetResult` since the callbacks fires twice if the WatchStream
486+
// acknowledges the Write before the WriteStream.
487+
offlineCallbackDone.trySetResult(null);
488+
} else {
489+
// Online snapshot. Since the server timestamp is committed, we should be able to
490+
// query by it.
491+
collection
492+
.orderBy("timestamp")
493+
.endAt(docSnap)
494+
.addSnapshotListener((snapshot2, error2) -> {});
495+
onlineCallbackDone.trySetResult(null);
496+
}
497+
});
495498

496499
DocumentReference document = collection.document();
497500
document.set(map("timestamp", FieldValue.serverTimestamp()));
498501
waitFor(offlineCallbackDone.getTask());
499502

500503
waitFor(collection.firestore.getClient().enableNetwork());
501504
waitFor(onlineCallbackDone.getTask());
505+
506+
listenerRegistration.remove();
502507
}
503508

504509
@Test

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/WriteBatchTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ public void testWriteTheSameServerTimestampAcrossWrites() {
222222
assertTrue(localSnap.getMetadata().hasPendingWrites());
223223
assertEquals(asList(map("when", null), map("when", null)), querySnapshotToValues(localSnap));
224224

225-
QuerySnapshot serverSnap = accumulator.await();
225+
QuerySnapshot serverSnap = accumulator.awaitRemoteEvent();
226226
assertFalse(serverSnap.getMetadata().hasPendingWrites());
227227
assertEquals(2, serverSnap.size());
228228
Timestamp when = serverSnap.getDocuments().get(0).getTimestamp("when");

firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentSnapshot.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public Map<String, Object> getData(@NonNull ServerTimestampBehavior serverTimest
149149
firestore,
150150
firestore.getFirestoreSettings().areTimestampsInSnapshotsEnabled(),
151151
serverTimestampBehavior);
152-
return doc == null ? null : userDataWriter.convertObject(doc.getData());
152+
return doc == null ? null : userDataWriter.convertObject(doc.getData().getFieldsMap());
153153
}
154154

155155
/**
@@ -533,7 +533,7 @@ private Object getInternal(
533533
if (val != null) {
534534
UserDataWriter userDataWriter =
535535
new UserDataWriter(firestore, timestampsInSnapshots, serverTimestampBehavior);
536-
return userDataWriter.convertValue(val);
536+
return userDataWriter.convertValue(val.getProto());
537537
}
538538
}
539539
return null;

firebase-firestore/src/main/java/com/google/firebase/firestore/UserDataReader.java

Lines changed: 66 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,19 @@
3535
import com.google.firebase.firestore.model.mutation.FieldMask;
3636
import com.google.firebase.firestore.model.mutation.NumericIncrementTransformOperation;
3737
import com.google.firebase.firestore.model.mutation.ServerTimestampOperation;
38-
import com.google.firebase.firestore.model.value.ArrayValue;
39-
import com.google.firebase.firestore.model.value.BlobValue;
40-
import com.google.firebase.firestore.model.value.BooleanValue;
41-
import com.google.firebase.firestore.model.value.DoubleValue;
4238
import com.google.firebase.firestore.model.value.FieldValue;
43-
import com.google.firebase.firestore.model.value.GeoPointValue;
44-
import com.google.firebase.firestore.model.value.IntegerValue;
45-
import com.google.firebase.firestore.model.value.NullValue;
4639
import com.google.firebase.firestore.model.value.NumberValue;
4740
import com.google.firebase.firestore.model.value.ObjectValue;
48-
import com.google.firebase.firestore.model.value.ReferenceValue;
49-
import com.google.firebase.firestore.model.value.StringValue;
50-
import com.google.firebase.firestore.model.value.TimestampValue;
5141
import com.google.firebase.firestore.util.Assert;
5242
import com.google.firebase.firestore.util.CustomClassMapper;
5343
import com.google.firebase.firestore.util.Util;
44+
import com.google.firestore.v1.ArrayValue;
45+
import com.google.firestore.v1.MapValue;
46+
import com.google.firestore.v1.Value;
47+
import com.google.protobuf.NullValue;
48+
import com.google.type.LatLng;
5449
import java.util.ArrayList;
5550
import java.util.Date;
56-
import java.util.HashMap;
5751
import java.util.Iterator;
5852
import java.util.List;
5953
import java.util.Map;
@@ -130,8 +124,7 @@ public ParsedUpdateData parseUpdateData(Map<String, Object> data) {
130124
context.addToFieldMask(fieldPath);
131125
} else {
132126
@Nullable
133-
FieldValue parsedValue =
134-
convertAndParseFieldData(fieldValue, context.childContext(fieldPath));
127+
Value parsedValue = convertAndParseFieldData(fieldValue, context.childContext(fieldPath));
135128
if (parsedValue != null) {
136129
context.addToFieldMask(fieldPath);
137130
updateData.set(fieldPath, parsedValue);
@@ -181,8 +174,7 @@ public ParsedUpdateData parseUpdateData(List<Object> fieldsAndValues) {
181174
// Add it to the field mask, but don't add anything to updateData.
182175
context.addToFieldMask(parsedField);
183176
} else {
184-
FieldValue parsedValue =
185-
convertAndParseFieldData(fieldValue, context.childContext(parsedField));
177+
Value parsedValue = convertAndParseFieldData(fieldValue, context.childContext(parsedField));
186178
if (parsedValue != null) {
187179
context.addToFieldMask(parsedField);
188180
updateData.set(parsedField, parsedValue);
@@ -209,16 +201,16 @@ public FieldValue parseQueryValue(Object input, boolean allowArrays) {
209201
new ParseAccumulator(
210202
allowArrays ? UserData.Source.ArrayArgument : UserData.Source.Argument);
211203

212-
@Nullable FieldValue parsed = convertAndParseFieldData(input, accumulator.rootContext());
204+
@Nullable Value parsed = convertAndParseFieldData(input, accumulator.rootContext());
213205
hardAssert(parsed != null, "Parsed data should not be null.");
214206
hardAssert(
215207
accumulator.getFieldTransforms().isEmpty(),
216208
"Field transforms should have been disallowed.");
217-
return parsed;
209+
return FieldValue.valueOf(parsed);
218210
}
219211

220212
/** Converts a POJO to native types and then parses it into model types. */
221-
private FieldValue convertAndParseFieldData(Object input, ParseContext context) {
213+
private Value convertAndParseFieldData(Object input, ParseContext context) {
222214
Object converted = CustomClassMapper.convertToPlainJavaTypes(input);
223215
return parseData(converted, context);
224216
}
@@ -239,7 +231,7 @@ private ObjectValue convertAndParseDocumentData(Object input, ParseContext conte
239231
}
240232

241233
Object converted = CustomClassMapper.convertToPlainJavaTypes(input);
242-
FieldValue value = parseData(converted, context);
234+
FieldValue value = FieldValue.valueOf(parseData(converted, context));
243235

244236
if (!(value instanceof ObjectValue)) {
245237
throw new IllegalArgumentException(badDocReason + "of type: " + Util.typeName(input));
@@ -257,7 +249,7 @@ private ObjectValue convertAndParseDocumentData(Object input, ParseContext conte
257249
* not be included in the resulting parsed data.
258250
*/
259251
@Nullable
260-
private FieldValue parseData(Object input, ParseContext context) {
252+
private Value parseData(Object input, ParseContext context) {
261253
if (input instanceof Map) {
262254
return parseMap((Map<?, ?>) input, context);
263255

@@ -291,43 +283,42 @@ private FieldValue parseData(Object input, ParseContext context) {
291283
}
292284
}
293285

294-
private <K, V> ObjectValue parseMap(Map<K, V> map, ParseContext context) {
295-
Map<String, FieldValue> result = new HashMap<>();
296-
286+
private <K, V> Value parseMap(Map<K, V> map, ParseContext context) {
297287
if (map.isEmpty()) {
298288
if (context.getPath() != null && !context.getPath().isEmpty()) {
299289
context.addToFieldMask(context.getPath());
300290
}
301-
return ObjectValue.emptyObject();
291+
return Value.newBuilder().setMapValue(MapValue.getDefaultInstance()).build();
302292
} else {
293+
MapValue.Builder mapBuilder = MapValue.newBuilder();
303294
for (Entry<K, V> entry : map.entrySet()) {
304295
if (!(entry.getKey() instanceof String)) {
305296
throw context.createError(
306297
String.format("Non-String Map key (%s) is not allowed", entry.getValue()));
307298
}
308299
String key = (String) entry.getKey();
309-
@Nullable FieldValue parsedValue = parseData(entry.getValue(), context.childContext(key));
300+
@Nullable Value parsedValue = parseData(entry.getValue(), context.childContext(key));
310301
if (parsedValue != null) {
311-
result.put(key, parsedValue);
302+
mapBuilder.putFields(key, parsedValue);
312303
}
313304
}
305+
return Value.newBuilder().setMapValue(mapBuilder).build();
314306
}
315-
return ObjectValue.fromMap(result);
316307
}
317308

318-
private <T> ArrayValue parseList(List<T> list, ParseContext context) {
319-
List<FieldValue> result = new ArrayList<>(list.size());
309+
private <T> Value parseList(List<T> list, ParseContext context) {
310+
ArrayValue.Builder arrayBuilder = ArrayValue.newBuilder();
320311
int entryIndex = 0;
321312
for (T entry : list) {
322-
@Nullable FieldValue parsedEntry = parseData(entry, context.childContext(entryIndex));
313+
@Nullable Value parsedEntry = parseData(entry, context.childContext(entryIndex));
323314
if (parsedEntry == null) {
324315
// Just include nulls in the array for fields being replaced with a sentinel.
325-
parsedEntry = NullValue.nullValue();
316+
parsedEntry = Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build();
326317
}
327-
result.add(parsedEntry);
318+
arrayBuilder.addValues(parsedEntry);
328319
entryIndex++;
329320
}
330-
return ArrayValue.fromList(result);
321+
return Value.newBuilder().setArrayValue(arrayBuilder).build();
331322
}
332323

333324
/**
@@ -398,35 +389,37 @@ private void parseSentinelFieldValue(
398389
* @return The parsed value, or {@code null} if the value was a FieldValue sentinel that should
399390
* not be included in the resulting parsed data.
400391
*/
401-
private FieldValue parseScalarValue(Object input, ParseContext context) {
392+
private Value parseScalarValue(Object input, ParseContext context) {
402393
if (input == null) {
403-
return NullValue.nullValue();
394+
return Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build();
404395
} else if (input instanceof Integer) {
405-
return IntegerValue.valueOf(((Integer) input).longValue());
396+
return Value.newBuilder().setIntegerValue((Integer) input).build();
406397
} else if (input instanceof Long) {
407-
return IntegerValue.valueOf(((Long) input));
398+
return Value.newBuilder().setIntegerValue((Long) input).build();
408399
} else if (input instanceof Float) {
409-
return DoubleValue.valueOf(((Float) input).doubleValue());
400+
return Value.newBuilder().setDoubleValue(((Float) input).doubleValue()).build();
410401
} else if (input instanceof Double) {
411-
return DoubleValue.valueOf((Double) input);
402+
return Value.newBuilder().setDoubleValue((Double) input).build();
412403
} else if (input instanceof Boolean) {
413-
return BooleanValue.valueOf((Boolean) input);
404+
return Value.newBuilder().setBooleanValue((Boolean) input).build();
414405
} else if (input instanceof String) {
415-
return StringValue.valueOf((String) input);
406+
return Value.newBuilder().setStringValue((String) input).build();
416407
} else if (input instanceof Date) {
417-
return TimestampValue.valueOf(new Timestamp((Date) input));
408+
Timestamp timestamp = new Timestamp((Date) input);
409+
return parseTimestamp(timestamp);
418410
} else if (input instanceof Timestamp) {
419411
Timestamp timestamp = (Timestamp) input;
420-
long seconds = timestamp.getSeconds();
421-
// Firestore backend truncates precision down to microseconds. To ensure offline mode works
422-
// the same with regards to truncation, perform the truncation immediately without waiting for
423-
// the backend to do that.
424-
int truncatedNanoseconds = timestamp.getNanoseconds() / 1000 * 1000;
425-
return TimestampValue.valueOf(new Timestamp(seconds, truncatedNanoseconds));
412+
return parseTimestamp(timestamp);
426413
} else if (input instanceof GeoPoint) {
427-
return GeoPointValue.valueOf((GeoPoint) input);
414+
GeoPoint geoPoint = (GeoPoint) input;
415+
return Value.newBuilder()
416+
.setGeoPointValue(
417+
LatLng.newBuilder()
418+
.setLatitude(geoPoint.getLatitude())
419+
.setLongitude(geoPoint.getLongitude()))
420+
.build();
428421
} else if (input instanceof Blob) {
429-
return BlobValue.valueOf((Blob) input);
422+
return Value.newBuilder().setBytesValue(((Blob) input).toByteString()).build();
430423
} else if (input instanceof DocumentReference) {
431424
DocumentReference ref = (DocumentReference) input;
432425
// TODO: Rework once pre-converter is ported to Android.
@@ -442,14 +435,35 @@ private FieldValue parseScalarValue(Object input, ParseContext context) {
442435
databaseId.getDatabaseId()));
443436
}
444437
}
445-
return ReferenceValue.valueOf(databaseId, ref.getKey());
438+
return Value.newBuilder()
439+
.setReferenceValue(
440+
String.format(
441+
"projects/%s/databases/%s/documents/%s",
442+
databaseId.getProjectId(),
443+
databaseId.getDatabaseId(),
444+
((DocumentReference) input).getPath()))
445+
.build();
446446
} else if (input.getClass().isArray()) {
447447
throw context.createError("Arrays are not supported; use a List instead");
448448
} else {
449449
throw context.createError("Unsupported type: " + Util.typeName(input));
450450
}
451451
}
452452

453+
private Value parseTimestamp(Timestamp timestamp) {
454+
// Firestore backend truncates precision down to microseconds. To ensure offline mode works
455+
// the same with regards to truncation, perform the truncation immediately without waiting for
456+
// the backend to do that.
457+
int truncatedNanoseconds = timestamp.getNanoseconds() / 1000 * 1000;
458+
459+
return Value.newBuilder()
460+
.setTimestampValue(
461+
com.google.protobuf.Timestamp.newBuilder()
462+
.setSeconds(timestamp.getSeconds())
463+
.setNanos(truncatedNanoseconds))
464+
.build();
465+
}
466+
453467
private List<FieldValue> parseArrayTransformElements(List<Object> elements) {
454468
ParseAccumulator accumulator = new ParseAccumulator(UserData.Source.Argument);
455469

@@ -460,7 +474,7 @@ private List<FieldValue> parseArrayTransformElements(List<Object> elements) {
460474
// being unioned or removed are not considered writes since they cannot
461475
// contain any FieldValue sentinels, etc.
462476
ParseContext context = accumulator.rootContext();
463-
result.add(convertAndParseFieldData(element, context.childContext(i)));
477+
result.add(FieldValue.valueOf(convertAndParseFieldData(element, context.childContext(i))));
464478
}
465479
return result;
466480
}

0 commit comments

Comments
 (0)