Skip to content

Commit e7ac8b9

Browse files
Adding Proto-based equality and comparison (#1157)
1 parent baa0a89 commit e7ac8b9

File tree

5 files changed

+280
-29
lines changed

5 files changed

+280
-29
lines changed

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,6 @@ public int hashCode() {
8282

8383
@Override
8484
public int compareTo(@NonNull Blob other) {
85-
int size = Math.min(bytes.size(), other.bytes.size());
86-
for (int i = 0; i < size; i++) {
87-
// Make sure the bytes are unsigned
88-
int thisByte = bytes.byteAt(i) & 0xff;
89-
int otherByte = other.bytes.byteAt(i) & 0xff;
90-
if (thisByte < otherByte) {
91-
return -1;
92-
} else if (thisByte > otherByte) {
93-
return 1;
94-
}
95-
// Byte values are equal, continue with comparison
96-
}
97-
return Util.compareIntegers(bytes.size(), other.bytes.size());
85+
return Util.compareByteStrings(bytes, other.bytes);
9886
}
9987
}

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ public List<MutationBatch> getAllMutationBatchesAffectingDocumentKeys(
318318
Collections.sort(
319319
result,
320320
(MutationBatch lhs, MutationBatch rhs) ->
321-
Util.compareInts(lhs.getBatchId(), rhs.getBatchId()));
321+
Util.compareIntegers(lhs.getBatchId(), rhs.getBatchId()));
322322
}
323323
return result;
324324
}

firebase-firestore/src/main/java/com/google/firebase/firestore/model/value/FieldValue.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,17 @@
4242
*/
4343
public abstract class FieldValue implements Comparable<FieldValue> {
4444
/** The order of types in Firestore; this order is defined by the backend. */
45-
static final int TYPE_ORDER_NULL = 0;
45+
public static final int TYPE_ORDER_NULL = 0;
4646

47-
static final int TYPE_ORDER_BOOLEAN = 1;
48-
static final int TYPE_ORDER_NUMBER = 2;
49-
static final int TYPE_ORDER_TIMESTAMP = 3;
50-
static final int TYPE_ORDER_STRING = 4;
51-
static final int TYPE_ORDER_BLOB = 5;
52-
static final int TYPE_ORDER_REFERENCE = 6;
53-
static final int TYPE_ORDER_GEOPOINT = 7;
54-
static final int TYPE_ORDER_ARRAY = 8;
55-
static final int TYPE_ORDER_OBJECT = 9;
47+
public static final int TYPE_ORDER_BOOLEAN = 1;
48+
public static final int TYPE_ORDER_NUMBER = 2;
49+
public static final int TYPE_ORDER_TIMESTAMP = 3;
50+
public static final int TYPE_ORDER_STRING = 4;
51+
public static final int TYPE_ORDER_BLOB = 5;
52+
public static final int TYPE_ORDER_REFERENCE = 6;
53+
public static final int TYPE_ORDER_GEOPOINT = 7;
54+
public static final int TYPE_ORDER_ARRAY = 8;
55+
public static final int TYPE_ORDER_OBJECT = 9;
5656

5757
public abstract int typeOrder();
5858

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.model.value;
16+
17+
import static com.google.firebase.firestore.util.Assert.fail;
18+
19+
import androidx.annotation.Nullable;
20+
import com.google.common.base.Splitter;
21+
import com.google.firebase.firestore.util.Util;
22+
import com.google.firestore.v1.ArrayValue;
23+
import com.google.firestore.v1.MapValue;
24+
import com.google.firestore.v1.Value;
25+
import com.google.protobuf.Timestamp;
26+
import com.google.type.LatLng;
27+
import java.util.Iterator;
28+
import java.util.List;
29+
import java.util.Map;
30+
import java.util.TreeMap;
31+
32+
// TODO(mrschmidt): Make package-private
33+
public class ProtoValues {
34+
35+
public static int typeOrder(Value value) {
36+
37+
switch (value.getValueTypeCase()) {
38+
case NULL_VALUE:
39+
return FieldValue.TYPE_ORDER_NULL;
40+
case BOOLEAN_VALUE:
41+
return FieldValue.TYPE_ORDER_BOOLEAN;
42+
case INTEGER_VALUE:
43+
return FieldValue.TYPE_ORDER_NUMBER;
44+
case DOUBLE_VALUE:
45+
return FieldValue.TYPE_ORDER_NUMBER;
46+
case TIMESTAMP_VALUE:
47+
return FieldValue.TYPE_ORDER_TIMESTAMP;
48+
case STRING_VALUE:
49+
return FieldValue.TYPE_ORDER_STRING;
50+
case BYTES_VALUE:
51+
return FieldValue.TYPE_ORDER_BLOB;
52+
case REFERENCE_VALUE:
53+
return FieldValue.TYPE_ORDER_REFERENCE;
54+
case GEO_POINT_VALUE:
55+
return FieldValue.TYPE_ORDER_GEOPOINT;
56+
case ARRAY_VALUE:
57+
return FieldValue.TYPE_ORDER_ARRAY;
58+
case MAP_VALUE:
59+
return FieldValue.TYPE_ORDER_OBJECT;
60+
default:
61+
throw fail("Invalid value type: " + value.getValueTypeCase());
62+
}
63+
}
64+
65+
/** Returns whether `value` is non-null and corresponds to the given type order. */
66+
public static boolean isType(@Nullable Value value, int typeOrder) {
67+
return value != null && typeOrder(value) == typeOrder;
68+
}
69+
70+
public static boolean equals(Value left, Value right) {
71+
int leftType = typeOrder(left);
72+
int rightType = typeOrder(right);
73+
if (leftType != rightType) {
74+
return false;
75+
}
76+
77+
switch (leftType) {
78+
case FieldValue.TYPE_ORDER_NUMBER:
79+
return numberEquals(left, right);
80+
case FieldValue.TYPE_ORDER_ARRAY:
81+
return arrayEquals(left, right);
82+
case FieldValue.TYPE_ORDER_OBJECT:
83+
return objectEquals(left, right);
84+
default:
85+
return left.equals(right);
86+
}
87+
}
88+
89+
private static boolean numberEquals(Value left, Value right) {
90+
if (left.getValueTypeCase() == Value.ValueTypeCase.INTEGER_VALUE
91+
&& right.getValueTypeCase() == Value.ValueTypeCase.INTEGER_VALUE) {
92+
return left.equals(right);
93+
} else if (left.getValueTypeCase() == Value.ValueTypeCase.DOUBLE_VALUE
94+
&& right.getValueTypeCase() == Value.ValueTypeCase.DOUBLE_VALUE) {
95+
return Double.doubleToLongBits(left.getDoubleValue())
96+
== Double.doubleToLongBits(right.getDoubleValue());
97+
}
98+
99+
return false;
100+
}
101+
102+
private static boolean arrayEquals(Value left, Value right) {
103+
ArrayValue leftArray = left.getArrayValue();
104+
ArrayValue rightArray = right.getArrayValue();
105+
106+
if (leftArray.getValuesCount() != rightArray.getValuesCount()) {
107+
return false;
108+
}
109+
110+
for (int i = 0; i < leftArray.getValuesCount(); ++i) {
111+
if (!equals(leftArray.getValues(i), rightArray.getValues(i))) {
112+
return false;
113+
}
114+
}
115+
116+
return true;
117+
}
118+
119+
private static boolean objectEquals(Value left, Value right) {
120+
MapValue leftMap = left.getMapValue();
121+
MapValue rightMap = right.getMapValue();
122+
123+
if (leftMap.getFieldsCount() != rightMap.getFieldsCount()) {
124+
return false;
125+
}
126+
127+
for (Map.Entry<String, Value> entry : leftMap.getFieldsMap().entrySet()) {
128+
Value otherEntry = rightMap.getFieldsMap().get(entry.getKey());
129+
if (!entry.getValue().equals(otherEntry)) {
130+
return false;
131+
}
132+
}
133+
134+
return true;
135+
}
136+
137+
public static int compare(Value left, Value right) {
138+
int leftType = typeOrder(left);
139+
int rightType = typeOrder(right);
140+
141+
if (leftType != rightType) {
142+
return Util.compareIntegers(leftType, rightType);
143+
}
144+
145+
switch (leftType) {
146+
case FieldValue.TYPE_ORDER_NULL:
147+
return 0;
148+
case FieldValue.TYPE_ORDER_BOOLEAN:
149+
return Util.compareBooleans(left.getBooleanValue(), right.getBooleanValue());
150+
case FieldValue.TYPE_ORDER_NUMBER:
151+
return compareNumbers(left, right);
152+
case FieldValue.TYPE_ORDER_TIMESTAMP:
153+
return compareTimestamps(left.getTimestampValue(), right.getTimestampValue());
154+
case FieldValue.TYPE_ORDER_STRING:
155+
return left.getStringValue().compareTo(right.getStringValue());
156+
case FieldValue.TYPE_ORDER_BLOB:
157+
return Util.compareByteStrings(left.getBytesValue(), right.getBytesValue());
158+
case FieldValue.TYPE_ORDER_REFERENCE:
159+
return compareReferences(left.getReferenceValue(), right.getReferenceValue());
160+
case FieldValue.TYPE_ORDER_GEOPOINT:
161+
return compareGeoPoints(left.getGeoPointValue(), right.getGeoPointValue());
162+
case FieldValue.TYPE_ORDER_ARRAY:
163+
return compareArrays(left.getArrayValue(), right.getArrayValue());
164+
case FieldValue.TYPE_ORDER_OBJECT:
165+
return compareMaps(left.getMapValue(), right.getMapValue());
166+
default:
167+
throw fail("Invalid value type: " + leftType);
168+
}
169+
}
170+
171+
private static int compareNumbers(Value left, Value right) {
172+
if (left.getValueTypeCase() == Value.ValueTypeCase.DOUBLE_VALUE) {
173+
double thisDouble = left.getDoubleValue();
174+
if (right.getValueTypeCase() == Value.ValueTypeCase.DOUBLE_VALUE) {
175+
return Util.compareDoubles(thisDouble, right.getDoubleValue());
176+
} else if (right.getValueTypeCase() == Value.ValueTypeCase.INTEGER_VALUE) {
177+
return Util.compareMixed(thisDouble, right.getIntegerValue());
178+
}
179+
} else if (left.getValueTypeCase() == Value.ValueTypeCase.INTEGER_VALUE) {
180+
long thisLong = left.getIntegerValue();
181+
if (right.getValueTypeCase() == Value.ValueTypeCase.INTEGER_VALUE) {
182+
return Util.compareLongs(thisLong, right.getIntegerValue());
183+
} else if (right.getValueTypeCase() == Value.ValueTypeCase.DOUBLE_VALUE) {
184+
return -1 * Util.compareMixed(right.getDoubleValue(), thisLong);
185+
}
186+
}
187+
188+
throw fail("Unexpected values: %s vs %s", left, right);
189+
}
190+
191+
private static int compareTimestamps(Timestamp left, Timestamp right) {
192+
int comparison = Util.compareLongs(left.getSeconds(), right.getSeconds());
193+
if (comparison != 0) {
194+
return comparison;
195+
}
196+
return Util.compareIntegers(left.getNanos(), right.getNanos());
197+
}
198+
199+
private static int compareReferences(String leftPath, String rightPath) {
200+
List<String> leftSegments = Splitter.on('/').splitToList(leftPath);
201+
List<String> rightSegments = Splitter.on('/').splitToList(rightPath);
202+
int minLength = Math.min(leftSegments.size(), rightSegments.size());
203+
for (int i = 0; i < minLength; i++) {
204+
int cmp = leftSegments.get(i).compareTo(rightSegments.get(i));
205+
if (cmp != 0) {
206+
return cmp;
207+
}
208+
}
209+
return Util.compareIntegers(leftSegments.size(), rightSegments.size());
210+
}
211+
212+
private static int compareGeoPoints(LatLng left, LatLng right) {
213+
int comparison = Util.compareDoubles(left.getLatitude(), right.getLatitude());
214+
if (comparison == 0) {
215+
return Util.compareDoubles(left.getLongitude(), right.getLongitude());
216+
}
217+
return comparison;
218+
}
219+
220+
private static int compareArrays(ArrayValue left, ArrayValue right) {
221+
int minLength = Math.min(left.getValuesCount(), right.getValuesCount());
222+
for (int i = 0; i < minLength; i++) {
223+
int cmp = compare(left.getValues(i), right.getValues(i));
224+
if (cmp != 0) {
225+
return cmp;
226+
}
227+
}
228+
return Util.compareIntegers(left.getValuesCount(), right.getValuesCount());
229+
}
230+
231+
private static int compareMaps(MapValue left, MapValue right) {
232+
Iterator<Map.Entry<String, Value>> iterator1 =
233+
new TreeMap<>(left.getFieldsMap()).entrySet().iterator();
234+
Iterator<Map.Entry<String, Value>> iterator2 =
235+
new TreeMap<>(right.getFieldsMap()).entrySet().iterator();
236+
while (iterator1.hasNext() && iterator2.hasNext()) {
237+
Map.Entry<String, Value> entry1 = iterator1.next();
238+
Map.Entry<String, Value> entry2 = iterator2.next();
239+
int keyCompare = entry1.getKey().compareTo(entry2.getKey());
240+
if (keyCompare != 0) {
241+
return keyCompare;
242+
}
243+
int valueCompare = compare(entry1.getValue(), entry2.getValue());
244+
if (valueCompare != 0) {
245+
return valueCompare;
246+
}
247+
}
248+
249+
// Only equal if both iterators are exhausted.
250+
return Util.compareBooleans(iterator1.hasNext(), iterator2.hasNext());
251+
}
252+
}

firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,6 @@ public static int compareLongs(long i1, long i2) {
8686
return NumberComparisonHelper.compareLongs(i1, i2);
8787
}
8888

89-
/** Utility function to compare ints. See {@link #compareLongs} for rationale. */
90-
public static int compareInts(int i1, int i2) {
91-
return NumberComparisonHelper.compareLongs(i1, i2);
92-
}
93-
9489
/** Utility function to compare doubles (using Firestore semantics for NaN). */
9590
public static int compareDoubles(double i1, double i2) {
9691
return NumberComparisonHelper.firestoreCompareDoubles(i1, i2);
@@ -222,4 +217,20 @@ public static void crashMainThread(RuntimeException exception) {
222217
throw exception;
223218
});
224219
}
220+
221+
public static int compareByteStrings(ByteString left, ByteString right) {
222+
int size = Math.min(left.size(), right.size());
223+
for (int i = 0; i < size; i++) {
224+
// Make sure the bytes are unsigned
225+
int thisByte = left.byteAt(i) & 0xff;
226+
int otherByte = right.byteAt(i) & 0xff;
227+
if (thisByte < otherByte) {
228+
return -1;
229+
} else if (thisByte > otherByte) {
230+
return 1;
231+
}
232+
// Byte values are equal, continue with comparison
233+
}
234+
return Util.compareIntegers(left.size(), right.size());
235+
}
225236
}

0 commit comments

Comments
 (0)