Skip to content

Commit 18146ec

Browse files
committed
Merge branch 'main' into PR #2165 to update
2 parents 86de417 + 09225e4 commit 18146ec

File tree

4 files changed

+136
-21
lines changed

4 files changed

+136
-21
lines changed

google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineUtils.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ static BooleanExpression toPipelineBooleanExpr(FilterInternal f) {
108108
case EQUAL:
109109
return and(field.exists(), field.equal(value));
110110
case NOT_EQUAL:
111-
return and(field.exists(), field.notEqual(value));
111+
return field.notEqual(value);
112112
case ARRAY_CONTAINS:
113113
return and(field.exists(), field.arrayContains(value));
114114
case IN:
@@ -119,8 +119,7 @@ static BooleanExpression toPipelineBooleanExpr(FilterInternal f) {
119119
return and(field.exists(), arrayContainsAny(field, Lists.newArrayList(valuesListAny)));
120120
case NOT_IN:
121121
List<Value> notInValues = value.getArrayValue().getValuesList();
122-
return and(
123-
field.exists(), not(Expression.equalAny(field, Lists.newArrayList(notInValues))));
122+
return not(Expression.equalAny(field, Lists.newArrayList(notInValues)));
124123
default:
125124
// Handle OPERATOR_UNSPECIFIED and UNRECOGNIZED cases as needed
126125
throw new IllegalArgumentException("Unsupported operator: " + comparisonFilter.operator);

google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -373,21 +373,30 @@ public boolean equals(Object o) {
373373
return Objects.equals(fieldReference, filter.fieldReference);
374374
}
375375

376-
public int compare(QueryDocumentSnapshot doc1, QueryDocumentSnapshot doc2) {
377-
String path = fieldReference.getFieldPath();
378-
if (FieldPath.isDocumentId(path)) {
379-
return direction.documentIdComparator.compare(doc1, doc2);
376+
@Override
377+
public int compare(QueryDocumentSnapshot left, QueryDocumentSnapshot right) {
378+
Value leftValue =
379+
left.extractField(FieldPath.fromDotSeparatedString(fieldReference.getFieldPath()));
380+
Value rightValue =
381+
right.extractField(FieldPath.fromDotSeparatedString(fieldReference.getFieldPath()));
382+
383+
// If the field isn't present, we treat it as a null value.
384+
if (leftValue == null) {
385+
leftValue =
386+
Value.newBuilder().setNullValue(com.google.protobuf.NullValue.NULL_VALUE).build();
387+
}
388+
if (rightValue == null) {
389+
rightValue =
390+
Value.newBuilder().setNullValue(com.google.protobuf.NullValue.NULL_VALUE).build();
380391
}
381-
FieldPath fieldPath = FieldPath.fromDotSeparatedString(path);
382-
Preconditions.checkState(
383-
doc1.contains(fieldPath) && doc2.contains(fieldPath),
384-
"Can only compare fields that exist in the DocumentSnapshot."
385-
+ " Please include the fields you are ordering on in your select() call.");
386-
Value v1 = doc1.extractField(fieldPath);
387-
Value v2 = doc2.extractField(fieldPath);
388392

389-
int cmp = com.google.cloud.firestore.Order.INSTANCE.compare(v1, v2);
390-
return (direction == Direction.ASCENDING) ? cmp : -cmp;
393+
int cmp = com.google.cloud.firestore.Order.INSTANCE.compare(leftValue, rightValue);
394+
395+
if (direction == Direction.DESCENDING) {
396+
cmp = -cmp;
397+
}
398+
399+
return cmp;
391400
}
392401
}
393402

@@ -2052,6 +2061,7 @@ Pipeline pipeline() {
20522061
}
20532062

20542063
// Orders
2064+
List<FieldOrder> explicitSortOrder = this.options.getFieldOrders();
20552065
List<FieldOrder> normalizedOrderBy = createImplicitOrderBy();
20562066
int size = normalizedOrderBy.size();
20572067
List<Field> fields = new ArrayList<>(size);
@@ -2066,14 +2076,25 @@ Pipeline pipeline() {
20662076
}
20672077
}
20682078

2069-
if (fields.size() == 1) {
2070-
ppl = ppl.where(fields.get(0).exists());
2071-
} else {
2079+
// Only add existence filters for fields that are explicitly ordered.
2080+
// Implicit order bys (e.g. from inequality filters) should not generate
2081+
// existence filters
2082+
// because the inequality filter itself will already generate an existence
2083+
// filter if needed,
2084+
// or arguably should not if it's a NOT_IN / NOT_EQUAL filter.
2085+
List<Field> existenceCheckFields = new ArrayList<>();
2086+
for (FieldOrder order : explicitSortOrder) {
2087+
existenceCheckFields.add(Field.ofServerPath(order.fieldReference.getFieldPath()));
2088+
}
2089+
2090+
if (existenceCheckFields.size() == 1) {
2091+
ppl = ppl.where(existenceCheckFields.get(0).exists());
2092+
} else if (existenceCheckFields.size() > 1) {
20722093
ppl =
20732094
ppl.where(
20742095
and(
2075-
fields.get(0).exists(),
2076-
fields.subList(1, fields.size()).stream()
2096+
existenceCheckFields.get(0).exists(),
2097+
existenceCheckFields.subList(1, existenceCheckFields.size()).stream()
20772098
.map((Field field) -> field.exists())
20782099
.toArray(BooleanExpression[]::new)));
20792100
}

google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryToPipelineTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,4 +628,64 @@ public void supportsOrOperator() throws Exception {
628628
firestore.pipeline().createFrom(query1).execute().get().getResults();
629629
verifyResults(snapshot, map("foo", 1L, "bar", 2L), map("foo", 3L, "bar", 10L));
630630
}
631+
632+
@Test
633+
public void testNotEqualIncludesMissingField() throws Exception {
634+
CollectionReference collRef =
635+
testCollectionWithDocs(
636+
ImmutableMap.of(
637+
"1", map("foo", 1L, "bar", 1L),
638+
"2", map("foo", 2L) // Missing "bar"
639+
));
640+
Query query1 = collRef.whereNotEqualTo("bar", 1L);
641+
List<PipelineResult> snapshot =
642+
firestore.pipeline().createFrom(query1).execute().get().getResults();
643+
// document "2" should be included because "bar" is missing, which is not equal to 1.
644+
verifyResults(snapshot, map("foo", 2L));
645+
}
646+
647+
@Test
648+
public void testNotInIncludesMissingField() throws Exception {
649+
CollectionReference collRef =
650+
testCollectionWithDocs(
651+
ImmutableMap.of(
652+
"1", map("foo", 1L, "bar", 1L),
653+
"2", map("foo", 2L) // Missing "bar"
654+
));
655+
Query query1 = collRef.whereNotIn("bar", Arrays.asList(1L));
656+
List<PipelineResult> snapshot =
657+
firestore.pipeline().createFrom(query1).execute().get().getResults();
658+
// document "2" should be included because "bar" is missing, which is not in [1].
659+
verifyResults(snapshot, map("foo", 2L));
660+
}
661+
662+
@Test
663+
public void testInequalityMaintainsExistenceFilter() throws Exception {
664+
CollectionReference collRef =
665+
testCollectionWithDocs(
666+
ImmutableMap.of(
667+
"1", map("foo", 1L, "bar", 0L),
668+
"2", map("foo", 2L) // Missing "bar"
669+
));
670+
Query query1 = collRef.whereLessThan("bar", 1L);
671+
List<PipelineResult> snapshot =
672+
firestore.pipeline().createFrom(query1).execute().get().getResults();
673+
// document "2" should be excluded because "bar" is missing.
674+
verifyResults(snapshot, map("foo", 1L, "bar", 0L));
675+
}
676+
677+
@Test
678+
public void testExplicitOrderMaintainsExistenceFilter() throws Exception {
679+
CollectionReference collRef =
680+
testCollectionWithDocs(
681+
ImmutableMap.of(
682+
"1", map("foo", 1L, "bar", 1L),
683+
"2", map("foo", 2L) // Missing "bar"
684+
));
685+
Query query1 = collRef.orderBy("bar");
686+
List<PipelineResult> snapshot =
687+
firestore.pipeline().createFrom(query1).execute().get().getResults();
688+
// document "2" should be excluded because "bar" is missing and we have explicit order.
689+
verifyResults(snapshot, map("foo", 1L, "bar", 1L));
690+
}
631691
}

google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryWatchTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,4 +1095,39 @@ private ListenResponse filter(int documentCount) {
10951095
response.setFilter(ExistenceFilter.newBuilder().setCount(documentCount).build());
10961096
return response.build();
10971097
}
1098+
1099+
@Test
1100+
public void testInequalityIncludesAndSortsMissingFields() throws Exception {
1101+
setDocument("doc1", map("key", 1));
1102+
setDocument("doc2", map("key", 2));
1103+
setDocument("doc3", map("other", 1)); // missing "key"
1104+
setDocument("doc4", map("key", null));
1105+
1106+
final Query query = randomColl.whereNotEqualTo("key", 1);
1107+
QuerySnapshotEventListener listener =
1108+
QuerySnapshotEventListener.builder().setInitialEventCount(1).build();
1109+
ListenerRegistration registration = query.addSnapshotListener(listener);
1110+
1111+
try {
1112+
listener.eventsCountDownLatch.awaitInitialEvents();
1113+
} finally {
1114+
registration.remove();
1115+
}
1116+
1117+
ListenerAssertions listenerAssertions = listener.assertions();
1118+
listenerAssertions.noError();
1119+
1120+
if (getFirestoreEdition() == FirestoreEdition.ENTERPRISE) {
1121+
// Expect doc2, doc3, doc4. doc1 excluded.
1122+
// Order: Missing/Null (which are equal in sort) < Number.
1123+
// Missing/Null sorted by __name__.
1124+
// doc3 < doc4.
1125+
// So: doc3, doc4, doc2.
1126+
List<String> expectedOrder = Arrays.asList("doc3", "doc4", "doc2");
1127+
assertEquals(expectedOrder, listenerAssertions.addedIds);
1128+
} else {
1129+
List<String> expectedOrder = singletonList("doc2");
1130+
assertEquals(expectedOrder, listenerAssertions.addedIds);
1131+
}
1132+
}
10981133
}

0 commit comments

Comments
 (0)