Skip to content

Commit 9ff2f41

Browse files
author
Brian Chen
authored
feat: add recursiveDelete() to Firestore (#622) (#649)
1 parent 8ad6c8e commit 9ff2f41

File tree

11 files changed

+1420
-26
lines changed

11 files changed

+1420
-26
lines changed

google-cloud-firestore/clirr-ignored-differences.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,4 +252,10 @@
252252
<to>*</to>
253253
</difference>
254254

255+
<!-- Recursive Delete -->
256+
<difference>
257+
<differenceType>7012</differenceType>
258+
<className>com/google/cloud/firestore/Firestore</className>
259+
<method>com.google.api.core.ApiFuture recursiveDelete(*)</method>
260+
</difference>
255261
</differences>

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -805,7 +805,17 @@ public void close() throws InterruptedException, ExecutionException {
805805
flushFuture.get();
806806
}
807807

808-
private void verifyNotClosedLocked() {
808+
/**
809+
* Used for verifying that the BulkWriter instance isn't closed when calling from outside this
810+
* class.
811+
*/
812+
void verifyNotClosed() {
813+
synchronized (lock) {
814+
verifyNotClosedLocked();
815+
}
816+
}
817+
818+
void verifyNotClosedLocked() {
809819
if (this.closed) {
810820
throw new IllegalStateException("BulkWriter has already been closed.");
811821
}

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

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,84 @@ void getAll(
193193
@Nonnull
194194
BulkWriter bulkWriter(BulkWriterOptions options);
195195

196+
/**
197+
* Recursively deletes all documents and subcollections at and under the specified level.
198+
*
199+
* <p>If any delete fails, the ApiFuture contains an error with an error message containing the
200+
* number of failed deletes and the stack trace of the last failed delete. The provided reference
201+
* is deleted regardless of whether all deletes succeeded.
202+
*
203+
* <p>recursiveDelete() uses a {@link BulkWriter} instance with default settings to perform the
204+
* deletes. To customize throttling rates or add success/error callbacks, pass in a custom
205+
* BulkWriter instance.
206+
*
207+
* @param reference The reference of the collection to delete.
208+
* @return An ApiFuture that completes when all deletes have been performed. The future fails with
209+
* an error if any of the deletes fail.
210+
*/
211+
@BetaApi
212+
@Nonnull
213+
ApiFuture<Void> recursiveDelete(CollectionReference reference);
214+
215+
/**
216+
* Recursively deletes all documents and subcollections at and under the specified level.
217+
*
218+
* <p>If any delete fails, the ApiFuture contains an error with an error message containing the
219+
* number of failed deletes and the stack trace of the last failed delete. The provided reference
220+
* is deleted regardless of whether all deletes succeeded.
221+
*
222+
* <p>recursiveDelete() uses a {@link BulkWriter} instance with default settings to perform the
223+
* deletes. To customize throttling rates or add success/error callbacks, pass in a custom
224+
* BulkWriter instance.
225+
*
226+
* @param reference The reference of the collection to delete.
227+
* @param bulkWriter A custom BulkWriter instance used to perform the deletes.
228+
* @return An ApiFuture that completes when all deletes have been performed. The future fails with
229+
* an error if any of the deletes fail.
230+
*/
231+
@BetaApi
232+
@Nonnull
233+
ApiFuture<Void> recursiveDelete(CollectionReference reference, BulkWriter bulkWriter);
234+
235+
/**
236+
* Recursively deletes all documents and subcollections at and under the specified level.
237+
*
238+
* <p>If any delete fails, the ApiFuture contains an error with an error message containing the
239+
* number of failed deletes and the stack trace of the last failed delete. The provided reference
240+
* is deleted regardless of whether all deletes succeeded.
241+
*
242+
* <p>recursiveDelete() uses a {@link BulkWriter} instance with default settings to perform the
243+
* deletes. To customize throttling rates or add success/error callbacks, pass in a custom
244+
* BulkWriter instance.
245+
*
246+
* @param reference The reference of the document to delete.
247+
* @return An ApiFuture that completes when all deletes have been performed. The future fails with
248+
* an error if any of the deletes fail.
249+
*/
250+
@BetaApi
251+
@Nonnull
252+
ApiFuture<Void> recursiveDelete(DocumentReference reference);
253+
254+
/**
255+
* Recursively deletes all documents and subcollections at and under the specified level.
256+
*
257+
* <p>If any delete fails, the ApiFuture contains an error with an error message containing the
258+
* number of failed deletes and the stack trace of the last failed delete. The provided reference
259+
* is deleted regardless of whether all deletes succeeded.
260+
*
261+
* <p>recursiveDelete() uses a {@link BulkWriter} instance with default settings to perform the
262+
* deletes. To customize throttling rates or add success/error callbacks, pass in a custom
263+
* BulkWriter instance.
264+
*
265+
* @param reference The reference of the document to delete.
266+
* @param bulkWriter A custom BulkWriter instance used to perform the deletes.
267+
* @return An ApiFuture that completes when all deletes have been performed. The future fails with
268+
* an error if any of the deletes fail.
269+
*/
270+
@BetaApi
271+
@Nonnull
272+
ApiFuture<Void> recursiveDelete(DocumentReference reference, BulkWriter bulkWriter);
273+
196274
/**
197275
* Returns a FirestoreBundle.Builder {@link FirestoreBundle.Builder} instance using an
198276
* automatically generated bundle ID. When loaded on clients, client SDKs use the bundle ID and

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.api.gax.rpc.UnaryCallable;
2525
import com.google.cloud.Timestamp;
2626
import com.google.cloud.firestore.spi.v1.FirestoreRpc;
27+
import com.google.common.annotations.VisibleForTesting;
2728
import com.google.common.base.Preconditions;
2829
import com.google.common.collect.ImmutableMap;
2930
import com.google.firestore.v1.BatchGetDocumentsRequest;
@@ -59,6 +60,12 @@ class FirestoreImpl implements Firestore, FirestoreRpcContext<FirestoreImpl> {
5960
private final FirestoreOptions firestoreOptions;
6061
private final ResourcePath databasePath;
6162

63+
/**
64+
* A lazy-loaded BulkWriter instance to be used with recursiveDelete() if no BulkWriter instance
65+
* is provided.
66+
*/
67+
@Nullable private BulkWriter bulkWriterInstance;
68+
6269
private boolean closed;
6370

6471
FirestoreImpl(FirestoreOptions options) {
@@ -76,6 +83,14 @@ class FirestoreImpl implements Firestore, FirestoreRpcContext<FirestoreImpl> {
7683
ResourcePath.create(DatabaseRootName.of(options.getProjectId(), options.getDatabaseId()));
7784
}
7885

86+
/** Lazy-load the Firestore's default BulkWriter. */
87+
private BulkWriter getBulkWriter() {
88+
if (bulkWriterInstance == null) {
89+
bulkWriterInstance = bulkWriter();
90+
}
91+
return bulkWriterInstance;
92+
}
93+
7994
/** Creates a pseudo-random 20-character ID that can be used for Firestore documents. */
8095
static String autoId() {
8196
StringBuilder builder = new StringBuilder();
@@ -102,6 +117,47 @@ public BulkWriter bulkWriter(BulkWriterOptions options) {
102117
return new BulkWriter(this, options);
103118
}
104119

120+
@Nonnull
121+
public ApiFuture<Void> recursiveDelete(CollectionReference reference) {
122+
BulkWriter writer = getBulkWriter();
123+
return recursiveDelete(reference.getResourcePath(), writer);
124+
}
125+
126+
@Nonnull
127+
public ApiFuture<Void> recursiveDelete(CollectionReference reference, BulkWriter bulkWriter) {
128+
return recursiveDelete(reference.getResourcePath(), bulkWriter);
129+
}
130+
131+
@Nonnull
132+
public ApiFuture<Void> recursiveDelete(DocumentReference reference) {
133+
BulkWriter writer = getBulkWriter();
134+
return recursiveDelete(reference.getResourcePath(), writer);
135+
}
136+
137+
@Nonnull
138+
public ApiFuture<Void> recursiveDelete(
139+
DocumentReference reference, @Nonnull BulkWriter bulkWriter) {
140+
return recursiveDelete(reference.getResourcePath(), bulkWriter);
141+
}
142+
143+
@Nonnull
144+
public ApiFuture<Void> recursiveDelete(ResourcePath path, BulkWriter bulkWriter) {
145+
return recursiveDelete(
146+
path, bulkWriter, RecursiveDelete.MAX_PENDING_OPS, RecursiveDelete.MIN_PENDING_OPS);
147+
}
148+
149+
/**
150+
* This overload is not private in order to test the query resumption with startAfter() once the
151+
* RecursiveDelete instance has MAX_PENDING_OPS pending.
152+
*/
153+
@Nonnull
154+
@VisibleForTesting
155+
ApiFuture<Void> recursiveDelete(
156+
ResourcePath path, @Nonnull BulkWriter bulkWriter, int maxLimit, int minLimit) {
157+
RecursiveDelete deleter = new RecursiveDelete(this, bulkWriter, path, maxLimit, minLimit);
158+
return deleter.run();
159+
}
160+
105161
@Nonnull
106162
@Override
107163
public CollectionReference collection(@Nonnull String collectionPath) {

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

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -255,13 +255,24 @@ abstract static class QueryOptions {
255255

256256
abstract ImmutableList<FieldReference> getFieldProjections();
257257

258+
// Whether to select all documents under `parentPath`. By default, only
259+
// collections that match `collectionId` are selected.
260+
abstract boolean isKindless();
261+
262+
// Whether to require consistent documents when restarting the query. By
263+
// default, restarting the query uses the readTime offset of the original
264+
// query to provide consistent results.
265+
abstract boolean getRequireConsistency();
266+
258267
static Builder builder() {
259268
return new AutoValue_Query_QueryOptions.Builder()
260269
.setAllDescendants(false)
261270
.setLimitType(LimitType.First)
262271
.setFieldOrders(ImmutableList.<FieldOrder>of())
263272
.setFieldFilters(ImmutableList.<FieldFilter>of())
264-
.setFieldProjections(ImmutableList.<FieldReference>of());
273+
.setFieldProjections(ImmutableList.<FieldReference>of())
274+
.setKindless(false)
275+
.setRequireConsistency(true);
265276
}
266277

267278
abstract Builder toBuilder();
@@ -290,6 +301,10 @@ abstract static class Builder {
290301

291302
abstract Builder setFieldProjections(ImmutableList<FieldReference> value);
292303

304+
abstract Builder setKindless(boolean value);
305+
306+
abstract Builder setRequireConsistency(boolean value);
307+
293308
abstract QueryOptions build();
294309
}
295310
}
@@ -327,21 +342,21 @@ private static boolean isUnaryComparison(@Nullable Object value) {
327342
/** Computes the backend ordering semantics for DocumentSnapshot cursors. */
328343
private ImmutableList<FieldOrder> createImplicitOrderBy() {
329344
List<FieldOrder> implicitOrders = new ArrayList<>(options.getFieldOrders());
330-
boolean hasDocumentId = false;
331345

346+
// If no explicit ordering is specified, use the first inequality to define an implicit order.
332347
if (implicitOrders.isEmpty()) {
333-
// If no explicit ordering is specified, use the first inequality to define an implicit order.
334348
for (FieldFilter fieldFilter : options.getFieldFilters()) {
335349
if (fieldFilter.isInequalityFilter()) {
336350
implicitOrders.add(new FieldOrder(fieldFilter.fieldReference, Direction.ASCENDING));
337351
break;
338352
}
339353
}
340-
} else {
341-
for (FieldOrder fieldOrder : options.getFieldOrders()) {
342-
if (FieldPath.isDocumentId(fieldOrder.fieldReference.getFieldPath())) {
343-
hasDocumentId = true;
344-
}
354+
}
355+
356+
boolean hasDocumentId = false;
357+
for (FieldOrder fieldOrder : implicitOrders) {
358+
if (FieldPath.isDocumentId(fieldOrder.fieldReference.getFieldPath())) {
359+
hasDocumentId = true;
345360
}
346361
}
347362

@@ -1237,7 +1252,12 @@ BundledQuery toBundledQuery() {
12371252
private StructuredQuery.Builder buildWithoutClientTranslation() {
12381253
StructuredQuery.Builder structuredQuery = StructuredQuery.newBuilder();
12391254
CollectionSelector.Builder collectionSelector = CollectionSelector.newBuilder();
1240-
collectionSelector.setCollectionId(options.getCollectionId());
1255+
1256+
// Kindless queries select all descendant documents, so we don't add the collectionId field.
1257+
if (!options.isKindless()) {
1258+
collectionSelector.setCollectionId(options.getCollectionId());
1259+
}
1260+
12411261
collectionSelector.setAllDescendants(options.getAllDescendants());
12421262
structuredQuery.addFrom(collectionSelector);
12431263

@@ -1525,10 +1545,17 @@ public void onError(Throwable throwable) {
15251545
// since we are requiring at least a single document result.
15261546
QueryDocumentSnapshot cursor = lastReceivedDocument.get();
15271547
if (cursor != null) {
1528-
Query.this
1529-
.startAfter(cursor)
1530-
.internalStream(
1531-
documentObserver, /* transactionId= */ null, cursor.getReadTime());
1548+
if (options.getRequireConsistency()) {
1549+
Query.this
1550+
.startAfter(cursor)
1551+
.internalStream(
1552+
documentObserver, /* transactionId= */ null, cursor.getReadTime());
1553+
} else {
1554+
Query.this
1555+
.startAfter(cursor)
1556+
.internalStream(
1557+
documentObserver, /* transactionId= */ null, /* readTime= */ null);
1558+
}
15321559
}
15331560
} else {
15341561
Tracing.getTracer().getCurrentSpan().addAnnotation("Firestore.Query: Error");

0 commit comments

Comments
 (0)