Skip to content
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-firestore'
If you are using Gradle without BOM, add this to your dependencies:

```Groovy
implementation 'com.google.cloud:google-cloud-firestore:3.14.2'
implementation 'com.google.cloud:google-cloud-firestore:3.14.3'
```

If you are using SBT, add this to your dependencies:

```Scala
libraryDependencies += "com.google.cloud" % "google-cloud-firestore" % "3.14.2"
libraryDependencies += "com.google.cloud" % "google-cloud-firestore" % "3.14.3"
```
<!-- {x-version-update-end} -->

Expand Down Expand Up @@ -222,7 +222,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
[kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-firestore/java11.html
[stability-image]: https://img.shields.io/badge/stability-stable-green
[maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-firestore.svg
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-firestore/3.14.2
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-firestore/3.14.3
[authentication]: https://github.com/googleapis/google-cloud-java#authentication
[auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
[predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.firestore.v1.StructuredQuery;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;

Expand Down Expand Up @@ -83,6 +85,62 @@ static FieldPath fromDotSeparatedString(String field) {
return empty().append(field);
}

/**
* Creates a {@code FieldPath} from a server-encoded field path.
*
* <p>Copied from Firebase Android SDK:
* https://github.com/firebase/firebase-android-sdk/blob/2d3b2be7d2d00d693eb74986f20a6265c918848f/firebase-firestore/src/main/java/com/google/firebase/firestore/model/FieldPath.java#L47
*/
public static FieldPath fromServerFormat(String path) {
List<String> res = new ArrayList<>();
StringBuilder builder = new StringBuilder();

int i = 0;

// If we're inside '`' backticks, then we should ignore '.' dots.
boolean inBackticks = false;

while (i < path.length()) {
char c = path.charAt(i);
if (c == '\\') {
if (i + 1 == path.length()) {
throw new IllegalArgumentException("Trailing escape character is not allowed");
}
i++;
builder.append(path.charAt(i));
} else if (c == '.') {
if (!inBackticks) {
String elem = builder.toString();
if (elem.isEmpty()) {
throw new IllegalArgumentException(
"Invalid field path ("
+ path
+ "). Paths must not be empty, begin with '.', end with '.', or contain '..'");
}
builder = new StringBuilder();
res.add(elem);
} else {
// escaped, append to current segment
builder.append(c);
}
} else if (c == '`') {
inBackticks = !inBackticks;
} else {
builder.append(c);
}
i++;
}
String lastElem = builder.toString();
if (lastElem.isEmpty()) {
throw new IllegalArgumentException(
"Invalid field path ("
+ path
+ "). Paths must not be empty, begin with '.', end with '.', or contain '..'");
}
res.add(lastElem);
return FieldPath.of(res.toArray(new String[0]));
}

/** Returns an empty field path. */
static FieldPath empty() {
// NOTE: This is not static since it would create a circular class dependency during
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,13 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;
Expand Down Expand Up @@ -285,7 +288,9 @@ boolean isInequalityFilter() {
return operator.equals(GREATER_THAN)
|| operator.equals(GREATER_THAN_OR_EQUAL)
|| operator.equals(LESS_THAN)
|| operator.equals(LESS_THAN_OR_EQUAL);
|| operator.equals(LESS_THAN_OR_EQUAL)
|| operator.equals(NOT_EQUAL)
|| operator.equals(NOT_IN);
}

@Nullable
Expand Down Expand Up @@ -327,6 +332,11 @@ static final class FieldOrder {
this.direction = direction;
}

FieldOrder(String field, Direction direction) {
this.fieldReference = FieldPath.fromServerFormat(field).toProto();
this.direction = direction;
}

Order toProto() {
Order.Builder result = Order.newBuilder();
result.setField(fieldReference);
Expand Down Expand Up @@ -462,39 +472,57 @@ private static boolean isUnaryComparison(@Nullable Object value) {
return value == null || value.equals(Double.NaN) || value.equals(Float.NaN);
}

/** Computes the backend ordering semantics for DocumentSnapshot cursors. */
private ImmutableList<FieldOrder> createImplicitOrderBy() {
List<FieldOrder> implicitOrders = new ArrayList<>(options.getFieldOrders());

// If no explicit ordering is specified, use the first inequality to define an implicit order.
if (implicitOrders.isEmpty()) {
for (FilterInternal filter : options.getFilters()) {
FieldReference fieldReference = filter.getFirstInequalityField();
if (fieldReference != null) {
implicitOrders.add(new FieldOrder(fieldReference, Direction.ASCENDING));
break;
/** Returns the sorted set of inequality filter fields used in this query. */
private SortedSet<FieldPath> getInequalityFilterFields() {
SortedSet<FieldPath> result = new TreeSet<>();

for (FilterInternal filter : options.getFilters()) {
for (FieldFilterInternal subFilter : filter.getFlattenedFilters()) {
if (subFilter.isInequalityFilter()) {
result.add(FieldPath.fromServerFormat(subFilter.fieldReference.getFieldPath()));
}
}
}

boolean hasDocumentId = false;
for (FieldOrder fieldOrder : implicitOrders) {
if (FieldPath.isDocumentId(fieldOrder.fieldReference.getFieldPath())) {
hasDocumentId = true;
return result;
}

/** Computes the backend ordering semantics for DocumentSnapshot cursors. */
ImmutableList<FieldOrder> createImplicitOrderBy() {
// Any explicit order by fields should be added as is.
List<FieldOrder> result = new ArrayList<>(options.getFieldOrders());

HashSet<String> fieldsNormalized = new HashSet<>();
for (FieldOrder order : result) {
fieldsNormalized.add(order.fieldReference.getFieldPath());
}

/** The order of the implicit ordering always matches the last explicit order by. */
Direction lastDirection =
result.isEmpty() ? Direction.ASCENDING : result.get(result.size() - 1).direction;

/**
* Any inequality fields not explicitly ordered should be implicitly ordered in a
* lexicographical order. When there are multiple inequality filters on the same field, the
* field should be added only once.
*
* <p>Note: `SortedSet<FieldPath>` sorts the key field before other fields. However, we want the
* key field to be sorted last.
*/
SortedSet<FieldPath> inequalityFields = getInequalityFilterFields();
for (FieldPath field : inequalityFields) {
if (!fieldsNormalized.contains(field.toString())
&& !FieldPath.isDocumentId(field.toString())) {
result.add(new FieldOrder(field.toProto(), lastDirection));
}
}

if (!hasDocumentId) {
// Add implicit sorting by name, using the last specified direction.
Direction lastDirection =
implicitOrders.isEmpty()
? Direction.ASCENDING
: implicitOrders.get(implicitOrders.size() - 1).direction;

implicitOrders.add(new FieldOrder(FieldPath.documentId().toProto(), lastDirection));
// Add the document key field to the last if it is not explicitly ordered.
if (!fieldsNormalized.contains(FieldPath.documentId().toString())) {
result.add(new FieldOrder(FieldPath.documentId().toProto(), lastDirection));
}

return ImmutableList.<FieldOrder>builder().addAll(implicitOrders).build();
return ImmutableList.<FieldOrder>builder().addAll(result).build();
}

private Cursor createCursor(
Expand All @@ -506,11 +534,12 @@ private Cursor createCursor(
if (FieldPath.isDocumentId(path)) {
fieldValues.add(documentSnapshot.getReference());
} else {
FieldPath fieldPath = FieldPath.fromDotSeparatedString(path);
FieldPath fieldPath = FieldPath.fromServerFormat(path);
Preconditions.checkArgument(
documentSnapshot.contains(fieldPath),
"Field '%s' is missing in the provided DocumentSnapshot. Please provide a document "
+ "that contains values for all specified orderBy() and where() constraints.");
+ "that contains values for all specified orderBy() and where() constraints.",
fieldPath);
fieldValues.add(documentSnapshot.get(fieldPath));
}
}
Expand Down
Loading