-
Notifications
You must be signed in to change notification settings - Fork 625
Protobuf-backed FieldValues #1184
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
Changes from all commits
1602062
804a1b7
3d5d7c2
120ca33
82c0095
98953a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -149,7 +149,7 @@ public Map<String, Object> getData(@NonNull ServerTimestampBehavior serverTimest | |
firestore, | ||
firestore.getFirestoreSettings().areTimestampsInSnapshotsEnabled(), | ||
serverTimestampBehavior); | ||
return doc == null ? null : userDataWriter.convertObject(doc.getData()); | ||
return doc == null ? null : userDataWriter.convertObject(doc.getData().getFieldsMap()); | ||
} | ||
|
||
/** | ||
|
@@ -533,7 +533,7 @@ private Object getInternal( | |
if (val != null) { | ||
UserDataWriter userDataWriter = | ||
new UserDataWriter(firestore, timestampsInSnapshots, serverTimestampBehavior); | ||
return userDataWriter.convertValue(val); | ||
return userDataWriter.convertValue(val.getProto()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like having overloads (or just a public interface) that accepts FieldValue/ObjectValue would make this code less cumbersome to deal with. Then again, if this is the only caller, maybe this is better. Just a thought; no action required. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This pattern is used in two places in the code and in a bunch of test cases in UserWriterTest. I added a helper to UserDataWriterTest, but kept the usages in the code the same. |
||
} | ||
} | ||
return null; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,25 +35,19 @@ | |
import com.google.firebase.firestore.model.mutation.FieldMask; | ||
import com.google.firebase.firestore.model.mutation.NumericIncrementTransformOperation; | ||
import com.google.firebase.firestore.model.mutation.ServerTimestampOperation; | ||
import com.google.firebase.firestore.model.value.ArrayValue; | ||
import com.google.firebase.firestore.model.value.BlobValue; | ||
import com.google.firebase.firestore.model.value.BooleanValue; | ||
import com.google.firebase.firestore.model.value.DoubleValue; | ||
import com.google.firebase.firestore.model.value.FieldValue; | ||
import com.google.firebase.firestore.model.value.GeoPointValue; | ||
import com.google.firebase.firestore.model.value.IntegerValue; | ||
import com.google.firebase.firestore.model.value.NullValue; | ||
import com.google.firebase.firestore.model.value.NumberValue; | ||
import com.google.firebase.firestore.model.value.ObjectValue; | ||
import com.google.firebase.firestore.model.value.ReferenceValue; | ||
import com.google.firebase.firestore.model.value.StringValue; | ||
import com.google.firebase.firestore.model.value.TimestampValue; | ||
import com.google.firebase.firestore.util.Assert; | ||
import com.google.firebase.firestore.util.CustomClassMapper; | ||
import com.google.firebase.firestore.util.Util; | ||
import com.google.firestore.v1.ArrayValue; | ||
import com.google.firestore.v1.MapValue; | ||
import com.google.firestore.v1.Value; | ||
import com.google.protobuf.NullValue; | ||
import com.google.type.LatLng; | ||
import java.util.ArrayList; | ||
import java.util.Date; | ||
import java.util.HashMap; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
@@ -130,8 +124,7 @@ public ParsedUpdateData parseUpdateData(Map<String, Object> data) { | |
context.addToFieldMask(fieldPath); | ||
} else { | ||
@Nullable | ||
FieldValue parsedValue = | ||
convertAndParseFieldData(fieldValue, context.childContext(fieldPath)); | ||
Value parsedValue = convertAndParseFieldData(fieldValue, context.childContext(fieldPath)); | ||
if (parsedValue != null) { | ||
context.addToFieldMask(fieldPath); | ||
updateData.set(fieldPath, parsedValue); | ||
|
@@ -181,8 +174,7 @@ public ParsedUpdateData parseUpdateData(List<Object> fieldsAndValues) { | |
// Add it to the field mask, but don't add anything to updateData. | ||
context.addToFieldMask(parsedField); | ||
} else { | ||
FieldValue parsedValue = | ||
convertAndParseFieldData(fieldValue, context.childContext(parsedField)); | ||
Value parsedValue = convertAndParseFieldData(fieldValue, context.childContext(parsedField)); | ||
if (parsedValue != null) { | ||
context.addToFieldMask(parsedField); | ||
updateData.set(parsedField, parsedValue); | ||
|
@@ -209,16 +201,16 @@ public FieldValue parseQueryValue(Object input, boolean allowArrays) { | |
new ParseAccumulator( | ||
allowArrays ? UserData.Source.ArrayArgument : UserData.Source.Argument); | ||
|
||
@Nullable FieldValue parsed = convertAndParseFieldData(input, accumulator.rootContext()); | ||
@Nullable Value parsed = convertAndParseFieldData(input, accumulator.rootContext()); | ||
hardAssert(parsed != null, "Parsed data should not be null."); | ||
hardAssert( | ||
accumulator.getFieldTransforms().isEmpty(), | ||
"Field transforms should have been disallowed."); | ||
return parsed; | ||
return FieldValue.valueOf(parsed); | ||
} | ||
|
||
/** Converts a POJO to native types and then parses it into model types. */ | ||
private FieldValue convertAndParseFieldData(Object input, ParseContext context) { | ||
private Value convertAndParseFieldData(Object input, ParseContext context) { | ||
Object converted = CustomClassMapper.convertToPlainJavaTypes(input); | ||
return parseData(converted, context); | ||
} | ||
|
@@ -239,7 +231,7 @@ private ObjectValue convertAndParseDocumentData(Object input, ParseContext conte | |
} | ||
|
||
Object converted = CustomClassMapper.convertToPlainJavaTypes(input); | ||
FieldValue value = parseData(converted, context); | ||
FieldValue value = FieldValue.valueOf(parseData(converted, context)); | ||
|
||
if (!(value instanceof ObjectValue)) { | ||
throw new IllegalArgumentException(badDocReason + "of type: " + Util.typeName(input)); | ||
|
@@ -257,7 +249,7 @@ private ObjectValue convertAndParseDocumentData(Object input, ParseContext conte | |
* not be included in the resulting parsed data. | ||
*/ | ||
@Nullable | ||
private FieldValue parseData(Object input, ParseContext context) { | ||
private Value parseData(Object input, ParseContext context) { | ||
if (input instanceof Map) { | ||
return parseMap((Map<?, ?>) input, context); | ||
|
||
|
@@ -291,43 +283,42 @@ private FieldValue parseData(Object input, ParseContext context) { | |
} | ||
} | ||
|
||
private <K, V> ObjectValue parseMap(Map<K, V> map, ParseContext context) { | ||
Map<String, FieldValue> result = new HashMap<>(); | ||
|
||
private <K, V> Value parseMap(Map<K, V> map, ParseContext context) { | ||
if (map.isEmpty()) { | ||
if (context.getPath() != null && !context.getPath().isEmpty()) { | ||
context.addToFieldMask(context.getPath()); | ||
} | ||
return ObjectValue.emptyObject(); | ||
return Value.newBuilder().setMapValue(MapValue.getDefaultInstance()).build(); | ||
} else { | ||
MapValue.Builder mapBuilder = MapValue.newBuilder(); | ||
for (Entry<K, V> entry : map.entrySet()) { | ||
if (!(entry.getKey() instanceof String)) { | ||
throw context.createError( | ||
String.format("Non-String Map key (%s) is not allowed", entry.getValue())); | ||
} | ||
String key = (String) entry.getKey(); | ||
@Nullable FieldValue parsedValue = parseData(entry.getValue(), context.childContext(key)); | ||
@Nullable Value parsedValue = parseData(entry.getValue(), context.childContext(key)); | ||
if (parsedValue != null) { | ||
result.put(key, parsedValue); | ||
mapBuilder.putFields(key, parsedValue); | ||
} | ||
} | ||
return Value.newBuilder().setMapValue(mapBuilder).build(); | ||
} | ||
return ObjectValue.fromMap(result); | ||
} | ||
|
||
private <T> ArrayValue parseList(List<T> list, ParseContext context) { | ||
List<FieldValue> result = new ArrayList<>(list.size()); | ||
private <T> Value parseList(List<T> list, ParseContext context) { | ||
ArrayValue.Builder arrayBuilder = ArrayValue.newBuilder(); | ||
int entryIndex = 0; | ||
for (T entry : list) { | ||
@Nullable FieldValue parsedEntry = parseData(entry, context.childContext(entryIndex)); | ||
@Nullable Value parsedEntry = parseData(entry, context.childContext(entryIndex)); | ||
if (parsedEntry == null) { | ||
// Just include nulls in the array for fields being replaced with a sentinel. | ||
parsedEntry = NullValue.nullValue(); | ||
parsedEntry = Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); | ||
} | ||
result.add(parsedEntry); | ||
arrayBuilder.addValues(parsedEntry); | ||
entryIndex++; | ||
} | ||
return ArrayValue.fromList(result); | ||
return Value.newBuilder().setArrayValue(arrayBuilder).build(); | ||
} | ||
|
||
/** | ||
|
@@ -398,35 +389,37 @@ private void parseSentinelFieldValue( | |
* @return The parsed value, or {@code null} if the value was a FieldValue sentinel that should | ||
* not be included in the resulting parsed data. | ||
*/ | ||
private FieldValue parseScalarValue(Object input, ParseContext context) { | ||
private Value parseScalarValue(Object input, ParseContext context) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code essentially duplicates There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a TODO for now. I would like to remove the individual field value wrappers first. |
||
if (input == null) { | ||
return NullValue.nullValue(); | ||
return Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); | ||
} else if (input instanceof Integer) { | ||
return IntegerValue.valueOf(((Integer) input).longValue()); | ||
return Value.newBuilder().setIntegerValue((Integer) input).build(); | ||
} else if (input instanceof Long) { | ||
return IntegerValue.valueOf(((Long) input)); | ||
return Value.newBuilder().setIntegerValue((Long) input).build(); | ||
} else if (input instanceof Float) { | ||
return DoubleValue.valueOf(((Float) input).doubleValue()); | ||
return Value.newBuilder().setDoubleValue(((Float) input).doubleValue()).build(); | ||
} else if (input instanceof Double) { | ||
return DoubleValue.valueOf((Double) input); | ||
return Value.newBuilder().setDoubleValue((Double) input).build(); | ||
} else if (input instanceof Boolean) { | ||
return BooleanValue.valueOf((Boolean) input); | ||
return Value.newBuilder().setBooleanValue((Boolean) input).build(); | ||
} else if (input instanceof String) { | ||
return StringValue.valueOf((String) input); | ||
return Value.newBuilder().setStringValue((String) input).build(); | ||
} else if (input instanceof Date) { | ||
return TimestampValue.valueOf(new Timestamp((Date) input)); | ||
Timestamp timestamp = new Timestamp((Date) input); | ||
return parseTimestamp(timestamp); | ||
} else if (input instanceof Timestamp) { | ||
Timestamp timestamp = (Timestamp) input; | ||
long seconds = timestamp.getSeconds(); | ||
// Firestore backend truncates precision down to microseconds. To ensure offline mode works | ||
// the same with regards to truncation, perform the truncation immediately without waiting for | ||
// the backend to do that. | ||
int truncatedNanoseconds = timestamp.getNanoseconds() / 1000 * 1000; | ||
return TimestampValue.valueOf(new Timestamp(seconds, truncatedNanoseconds)); | ||
return parseTimestamp(timestamp); | ||
} else if (input instanceof GeoPoint) { | ||
return GeoPointValue.valueOf((GeoPoint) input); | ||
GeoPoint geoPoint = (GeoPoint) input; | ||
return Value.newBuilder() | ||
.setGeoPointValue( | ||
LatLng.newBuilder() | ||
.setLatitude(geoPoint.getLatitude()) | ||
.setLongitude(geoPoint.getLongitude())) | ||
.build(); | ||
} else if (input instanceof Blob) { | ||
return BlobValue.valueOf((Blob) input); | ||
return Value.newBuilder().setBytesValue(((Blob) input).toByteString()).build(); | ||
} else if (input instanceof DocumentReference) { | ||
DocumentReference ref = (DocumentReference) input; | ||
// TODO: Rework once pre-converter is ported to Android. | ||
|
@@ -442,14 +435,35 @@ private FieldValue parseScalarValue(Object input, ParseContext context) { | |
databaseId.getDatabaseId())); | ||
} | ||
} | ||
return ReferenceValue.valueOf(databaseId, ref.getKey()); | ||
return Value.newBuilder() | ||
.setReferenceValue( | ||
String.format( | ||
"projects/%s/databases/%s/documents/%s", | ||
databaseId.getProjectId(), | ||
databaseId.getDatabaseId(), | ||
((DocumentReference) input).getPath())) | ||
.build(); | ||
} else if (input.getClass().isArray()) { | ||
throw context.createError("Arrays are not supported; use a List instead"); | ||
} else { | ||
throw context.createError("Unsupported type: " + Util.typeName(input)); | ||
} | ||
} | ||
|
||
private Value parseTimestamp(Timestamp timestamp) { | ||
// Firestore backend truncates precision down to microseconds. To ensure offline mode works | ||
// the same with regards to truncation, perform the truncation immediately without waiting for | ||
// the backend to do that. | ||
int truncatedNanoseconds = timestamp.getNanoseconds() / 1000 * 1000; | ||
|
||
return Value.newBuilder() | ||
.setTimestampValue( | ||
com.google.protobuf.Timestamp.newBuilder() | ||
.setSeconds(timestamp.getSeconds()) | ||
.setNanos(truncatedNanoseconds)) | ||
.build(); | ||
} | ||
|
||
private List<FieldValue> parseArrayTransformElements(List<Object> elements) { | ||
ParseAccumulator accumulator = new ParseAccumulator(UserData.Source.Argument); | ||
|
||
|
@@ -460,7 +474,7 @@ private List<FieldValue> parseArrayTransformElements(List<Object> elements) { | |
// being unioned or removed are not considered writes since they cannot | ||
// contain any FieldValue sentinels, etc. | ||
ParseContext context = accumulator.rootContext(); | ||
result.add(convertAndParseFieldData(element, context.childContext(i))); | ||
result.add(FieldValue.valueOf(convertAndParseFieldData(element, context.childContext(i)))); | ||
} | ||
return result; | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR somehow changed the timing of events so much that the
fromCache:false
event is now delivered beforehasPendingWrites:false
. I am not 100% convinced that this is not due to a bug, but if I look at the test logs everything looks sane.