Skip to content

Support > and != on the same field #3454

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

Merged
merged 1 commit into from
Feb 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@

package com.google.firebase.firestore.core;

import static com.google.firebase.firestore.model.Values.MAX_VALUE;
import static com.google.firebase.firestore.model.Values.MIN_VALUE;
import static com.google.firebase.firestore.model.Values.max;
import static com.google.firebase.firestore.model.Values.min;

Expand All @@ -25,10 +23,11 @@
import com.google.firebase.firestore.model.FieldPath;
import com.google.firebase.firestore.model.ResourcePath;
import com.google.firebase.firestore.model.Values;
import com.google.firestore.v1.ArrayValue;
import com.google.firestore.v1.Value;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;

/**
Expand Down Expand Up @@ -153,23 +152,21 @@ private List<FieldFilter> getFieldFiltersForPath(FieldPath path) {
* Returns the list of values that are used in != or NOT_IN filters. Returns {@code null} if there
* are no such filters.
*/
public @Nullable List<Value> getNotInValues(FieldIndex fieldIndex) {
List<Value> values = new ArrayList<>();

public @Nullable Collection<Value> getNotInValues(FieldIndex fieldIndex) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method now supports a == 1 && a != 2, which now correctly just returns a != 2 instead of the concatenation of both.

LinkedHashMap<FieldPath, Value> values = new LinkedHashMap<>();
for (FieldIndex.Segment segment : fieldIndex.getDirectionalSegments()) {
for (FieldFilter fieldFilter : getFieldFiltersForPath(segment.getFieldPath())) {
switch (fieldFilter.getOperator()) {
case EQUAL:
case IN:
// Encode equality prefix, which is encoded in the index value before the inequality
// (e.g. `a == 'a' && b != 'b'` is encoded to `value != 'ab'`).
values.add(fieldFilter.getValue());
values.put(segment.getFieldPath(), fieldFilter.getValue());
break;
case NOT_IN:
case NOT_EQUAL:
// NotIn/NotEqual is always a suffix
values.add(fieldFilter.getValue());
return values;
values.put(segment.getFieldPath(), fieldFilter.getValue());
return values.values(); // NotIn/NotEqual is always a suffix
}
}
}
Expand Down Expand Up @@ -211,13 +208,8 @@ public Bound getLowerBound(FieldIndex fieldIndex) {
filterInclusive = false;
break;
case NOT_EQUAL:
filterValue = Values.MIN_VALUE;
break;
case NOT_IN:
filterValue =
Value.newBuilder()
.setArrayValue(ArrayValue.newBuilder().addValues(MIN_VALUE))
.build();
filterValue = Values.MIN_VALUE;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not In and Not Equal use the same bound, which makes the comparison logic at the end of this method work as we do not want to compare to an array but to an actual minimum.

break;
default:
// Remaining filters cannot be used as lower bounds.
Expand Down Expand Up @@ -291,13 +283,8 @@ public Bound getLowerBound(FieldIndex fieldIndex) {
filterInclusive = false;
break;
case NOT_EQUAL:
filterValue = Values.MAX_VALUE;
break;
case NOT_IN:
filterValue =
Value.newBuilder()
.setArrayValue(ArrayValue.newBuilder().addValues(MAX_VALUE))
.build();
filterValue = Values.MAX_VALUE;
break;
default:
// Remaining filters cannot be used as upper bounds.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ public Set<DocumentKey> getDocumentsMatchingTarget(Target target) {
}

@Nullable List<Value> arrayValues = subTarget.getArrayValues(fieldIndex);
@Nullable List<Value> notInValues = subTarget.getNotInValues(fieldIndex);
@Nullable Collection<Value> notInValues = subTarget.getNotInValues(fieldIndex);
@Nullable Bound lowerBound = subTarget.getLowerBound(fieldIndex);
@Nullable Bound upperBound = subTarget.getUpperBound(fieldIndex);

Expand Down Expand Up @@ -564,7 +564,7 @@ private Object[] generateQueryAndBindings(
Object[] bindArgs =
fillBounds(statementCount, indexId, arrayValues, lowerBounds, upperBounds, notIn);

List<Object> result = new ArrayList<Object>();
List<Object> result = new ArrayList<>();
result.add(sql.toString());
result.addAll(Arrays.asList(bindArgs));
return result.toArray();
Expand Down Expand Up @@ -673,7 +673,7 @@ private byte[] encodeSingleElement(Value value) {
* queries, a list of possible values is returned.
*/
private @Nullable Object[] encodeValues(
FieldIndex fieldIndex, Target target, @Nullable List<Value> values) {
FieldIndex fieldIndex, Target target, @Nullable Collection<Value> values) {
if (values == null) return null;

List<IndexByteEncoder> encoders = new ArrayList<>();
Expand Down Expand Up @@ -740,8 +740,10 @@ private boolean isInFilter(Target target, FieldPath fieldPath) {
for (Filter filter : target.getFilters()) {
if ((filter instanceof FieldFilter) && ((FieldFilter) filter).getField().equals(fieldPath)) {
FieldFilter.Operator operator = ((FieldFilter) filter).getOperator();
return operator.equals(FieldFilter.Operator.IN)
|| operator.equals(FieldFilter.Operator.NOT_IN);
if (operator.equals(FieldFilter.Operator.IN)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A not-in filter does not have to appear on the first filter for a field. If there are multiple filters for the field path, not in can appear later.

|| operator.equals(FieldFilter.Operator.NOT_IN)) {
return true;
}
}
}
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,15 @@ public class Values {
public static final Value NULL_VALUE =
Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build();
public static final Value MIN_VALUE = NULL_VALUE;
private static final Value MAX_VALUE_TYPE = Value.newBuilder().setStringValue("__max__").build();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All changes in this file are to support correct ordering for MAX_VALUE.

public static final Value MAX_VALUE =
Value.newBuilder()
.setMapValue(
MapValue.newBuilder()
.putFields("__type__", Value.newBuilder().setStringValue("__max__").build()))
.setMapValue(MapValue.newBuilder().putFields("__type__", MAX_VALUE_TYPE))
.build();

/**
* The order of types in Firestore. This order is based on the backend's ordering, but modified to
* support server timestamps.
* support server timestamps and {@link #MAX_VALUE}.
*/
public static final int TYPE_ORDER_NULL = 0;

Expand All @@ -65,6 +64,8 @@ public class Values {
public static final int TYPE_ORDER_ARRAY = 9;
public static final int TYPE_ORDER_MAP = 10;

public static final int TYPE_ORDER_MAX_VALUE = Integer.MAX_VALUE;

/** Returns the backend's type order of the given Value type. */
public static int typeOrder(Value value) {
switch (value.getValueTypeCase()) {
Expand All @@ -91,8 +92,11 @@ public static int typeOrder(Value value) {
case MAP_VALUE:
if (isServerTimestamp(value)) {
return TYPE_ORDER_SERVER_TIMESTAMP;
} else if (isMaxValue(value)) {
return TYPE_ORDER_MAX_VALUE;
} else {
return TYPE_ORDER_MAP;
}
return TYPE_ORDER_MAP;
default:
throw fail("Invalid value type: " + value.getValueTypeCase());
}
Expand Down Expand Up @@ -122,6 +126,8 @@ public static boolean equals(Value left, Value right) {
return objectEquals(left, right);
case TYPE_ORDER_SERVER_TIMESTAMP:
return getLocalWriteTime(left).equals(getLocalWriteTime(right));
case TYPE_ORDER_MAX_VALUE:
return true;
default:
return left.equals(right);
}
Expand Down Expand Up @@ -195,6 +201,7 @@ public static int compare(Value left, Value right) {

switch (leftType) {
case TYPE_ORDER_NULL:
case TYPE_ORDER_MAX_VALUE:
return 0;
case TYPE_ORDER_BOOLEAN:
return Util.compareBooleans(left.getBooleanValue(), right.getBooleanValue());
Expand Down Expand Up @@ -531,6 +538,6 @@ public static Value getUpperBound(Value.ValueTypeCase valueTypeCase) {

/** Returns true if the Value represents the canonical {@link #MAX_VALUE} . */
public static boolean isMaxValue(Value value) {
return equals(value, MAX_VALUE);
return MAX_VALUE_TYPE.equals(value.getMapValue().getFieldsMap().get("__type__"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,19 @@ public void testEqualsWithNotEqualsFilter() {
verifyResults(query, "coll/val2");
}

@Test
public void testEqualsWithNotEqualsFilterSameField() {
setUpSingleValueFilter();
Query query = query("coll").filter(filter("count", ">", 1)).filter(filter("count", "!=", 2));
verifyResults(query, "coll/val3");

query = query("coll").filter(filter("count", "==", 1)).filter(filter("count", "!=", 2));
verifyResults(query, "coll/val1");

query = query("coll").filter(filter("count", "==", 1)).filter(filter("count", "!=", 1));
verifyResults(query);
}

@Test
public void testLessThanFilter() {
setUpSingleValueFilter();
Expand Down Expand Up @@ -227,6 +240,26 @@ public void testNotInFilter() {
verifyResults(query, "coll/val3");
}

@Test
public void testNotInWithGreaterThanFilter() {
setUpSingleValueFilter();
Query query =
query("coll")
.filter(filter("count", ">", 1))
.filter(filter("count", "not-in", Collections.singletonList(2)));
verifyResults(query, "coll/val3");
}

@Test
public void testOutOfBoundsNotInWithGreaterThanFilter() {
setUpSingleValueFilter();
Query query =
query("coll")
.filter(filter("count", ">", 2))
.filter(filter("count", "not-in", Collections.singletonList(1)));
verifyResults(query, "coll/val3");
}

@Test
public void testArrayContainsFilter() {
setUpArrayValueFilter();
Expand Down