21
21
import static com .google .firebase .firestore .util .Util .repeatSequence ;
22
22
23
23
import android .database .Cursor ;
24
+ import android .util .Log ;
24
25
import androidx .annotation .NonNull ;
25
26
import androidx .annotation .VisibleForTesting ;
26
27
import com .google .firebase .Timestamp ;
41
42
import java .util .Collection ;
42
43
import java .util .Collections ;
43
44
import java .util .HashMap ;
45
+ import java .util .Iterator ;
44
46
import java .util .List ;
45
47
import java .util .Map ;
46
48
import java .util .Set ;
49
+ import java .util .concurrent .ConcurrentHashMap ;
47
50
import java .util .concurrent .Executor ;
48
51
import javax .annotation .Nonnull ;
49
52
import javax .annotation .Nullable ;
@@ -56,6 +59,8 @@ final class SQLiteRemoteDocumentCache implements RemoteDocumentCache {
56
59
private final LocalSerializer serializer ;
57
60
private IndexManager indexManager ;
58
61
62
+ private final DocumentTypeBackfills documentTypeBackfills = new DocumentTypeBackfills ();
63
+
59
64
SQLiteRemoteDocumentCache (SQLitePersistence persistence , LocalSerializer serializer ) {
60
65
this .db = persistence ;
61
66
this .serializer = serializer ;
@@ -168,17 +173,15 @@ public Map<DocumentKey, MutableDocument> getAll(Iterable<DocumentKey> documentKe
168
173
") ORDER BY path" );
169
174
170
175
BackgroundQueue backgroundQueue = new BackgroundQueue ();
171
- ArrayList <DocumentTypeUpdateInfo > documentTypeBackfills = new ArrayList <>();
172
176
while (longQuery .hasMoreSubqueries ()) {
173
177
longQuery
174
178
.performNextSubquery ()
175
- .forEach (
176
- row ->
177
- processRowInBackground (
178
- backgroundQueue , results , row , documentTypeBackfills , /*filter*/ null ));
179
+ .forEach (row -> processRowInBackground (backgroundQueue , results , row , /*filter*/ null ));
179
180
}
180
181
backgroundQueue .drain ();
181
182
183
+ documentTypeBackfills .backfill (db );
184
+
182
185
synchronized (results ) {
183
186
return results ;
184
187
}
@@ -265,18 +268,19 @@ private Map<DocumentKey, MutableDocument> getAll(
265
268
266
269
BackgroundQueue backgroundQueue = new BackgroundQueue ();
267
270
Map <DocumentKey , MutableDocument > results = new HashMap <>();
268
- ArrayList <DocumentTypeUpdateInfo > documentTypeBackfills = new ArrayList <>();
269
271
db .query (sql .toString ())
270
272
.binding (bindVars )
271
273
.forEach (
272
274
row -> {
273
- processRowInBackground (backgroundQueue , results , row , documentTypeBackfills , filter );
275
+ processRowInBackground (backgroundQueue , results , row , filter );
274
276
if (context != null ) {
275
277
context .incrementDocumentReadCount ();
276
278
}
277
279
});
278
280
backgroundQueue .drain ();
279
281
282
+ documentTypeBackfills .backfill (db );
283
+
280
284
synchronized (results ) {
281
285
return results ;
282
286
}
@@ -295,7 +299,6 @@ private void processRowInBackground(
295
299
BackgroundQueue backgroundQueue ,
296
300
Map <DocumentKey , MutableDocument > results ,
297
301
Cursor row ,
298
- List <DocumentTypeUpdateInfo > documentTypeBackfills ,
299
302
@ Nullable Function <MutableDocument , Boolean > filter ) {
300
303
byte [] rawDocument = row .getBlob (0 );
301
304
int readTimeSeconds = row .getInt (1 );
@@ -311,15 +314,7 @@ private void processRowInBackground(
311
314
MutableDocument document =
312
315
decodeMaybeDocument (rawDocument , readTimeSeconds , readTimeNanos );
313
316
if (documentTypeIsNull ) {
314
- DocumentTypeUpdateInfo updateInfo =
315
- new DocumentTypeUpdateInfo (
316
- path ,
317
- readTimeSeconds ,
318
- readTimeNanos ,
319
- DocumentType .forMutableDocument (document ));
320
- synchronized (documentTypeBackfills ) {
321
- documentTypeBackfills .add (updateInfo );
322
- }
317
+ documentTypeBackfills .add (path , readTimeSeconds , readTimeNanos , document );
323
318
}
324
319
if (filter == null || filter .apply (document )) {
325
320
synchronized (results ) {
@@ -364,32 +359,106 @@ private MutableDocument decodeMaybeDocument(
364
359
}
365
360
}
366
361
367
- private static class DocumentTypeUpdateInfo {
368
- final String path ;
369
- final int readTimeSeconds ;
370
- final int readTimeNanos ;
371
- final DocumentType documentType ;
372
-
373
- DocumentTypeUpdateInfo (
374
- String path , int readTimeSeconds , int readTimeNanos , DocumentType documentType ) {
375
- this .path = path ;
376
- this .readTimeSeconds = readTimeSeconds ;
377
- this .readTimeNanos = readTimeNanos ;
378
- this .documentType = documentType ;
362
+ // This class is thread safe and all public methods may be safely called concurrently from
363
+ // multiple threads. This makes it safe to use instances of this class from BackgroundQueue.
364
+ private static class DocumentTypeBackfills {
365
+
366
+ private final ConcurrentHashMap <BackfillKey , DocumentType > documentTypeByBackfillKey =
367
+ new ConcurrentHashMap <>();
368
+
369
+ enum BackfillResult {
370
+ NO_PENDING_BACKFILLS ,
371
+ HAS_PENDING_BACKFILLS ,
372
+ }
373
+
374
+ void add (String path , int readTimeSeconds , int readTimeNanos , MutableDocument document ) {
375
+ BackfillKey backfillKey = new BackfillKey (path , readTimeSeconds , readTimeNanos );
376
+ DocumentType documentType = DocumentType .forMutableDocument (document );
377
+ documentTypeByBackfillKey .putIfAbsent (backfillKey , documentType );
378
+ }
379
+
380
+ BackfillResult backfill (SQLitePersistence db ) {
381
+ ArrayList <String > caseClauses = new ArrayList <>();
382
+ ArrayList <Object > caseClauseBindings = new ArrayList <>();
383
+ ArrayList <String > whereClauses = new ArrayList <>();
384
+ ArrayList <Object > whereClauseBindings = new ArrayList <>();
385
+
386
+ Iterator <BackfillKey > backfillKeys = documentTypeByBackfillKey .keySet ().iterator ();
387
+ while (backfillKeys .hasNext ()
388
+ && caseClauseBindings .size () + whereClauseBindings .size () < 900 ) {
389
+ BackfillKey backfillKey = backfillKeys .next ();
390
+ DocumentType documentType = documentTypeByBackfillKey .remove (backfillKey );
391
+ if (documentType == null ) {
392
+ continue ;
393
+ }
394
+
395
+ caseClauses .add ("WHEN path=? AND read_time_seconds=? AND read_time_nanos=? THEN ?" );
396
+ caseClauseBindings .add (backfillKey .path );
397
+ caseClauseBindings .add (backfillKey .readTimeSeconds );
398
+ caseClauseBindings .add (backfillKey .readTimeNanos );
399
+ caseClauseBindings .add (documentType .dbValue );
400
+
401
+ whereClauses .add ("(path=? AND read_time_seconds=? AND read_time_nanos=?)" );
402
+ whereClauseBindings .add (backfillKey .path );
403
+ whereClauseBindings .add (backfillKey .readTimeSeconds );
404
+ whereClauseBindings .add (backfillKey .readTimeNanos );
405
+ }
406
+
407
+ if (!caseClauseBindings .isEmpty ()) {
408
+ String sql ;
409
+ {
410
+ StringBuilder sb = new StringBuilder ("UPDATE remote_documents SET document_type = CASE" );
411
+ for (String caseClause : caseClauses ) {
412
+ sb .append (' ' ).append (caseClause );
413
+ }
414
+ sb .append (" ELSE NULL END WHERE " );
415
+ boolean isFirstWhereClause = true ;
416
+ for (String whereClause : whereClauses ) {
417
+ if (isFirstWhereClause ) {
418
+ isFirstWhereClause = false ;
419
+ } else {
420
+ sb .append (" OR " );
421
+ }
422
+ sb .append (whereClause );
423
+ }
424
+ sql = sb .toString ();
425
+ }
426
+
427
+ caseClauseBindings .addAll (whereClauseBindings );
428
+ Object [] bindings = caseClauseBindings .toArray ();
429
+
430
+ Log .i ("zzyzx" , "sql=sql" );
431
+
432
+ db .execute (sql , bindings );
433
+ }
434
+
435
+ return documentTypeByBackfillKey .isEmpty ()
436
+ ? BackfillResult .NO_PENDING_BACKFILLS
437
+ : BackfillResult .HAS_PENDING_BACKFILLS ;
379
438
}
380
439
381
- @ NonNull
382
- @ Override
383
- public String toString () {
384
- return "DocumentTypeUpdateInfo(path="
385
- + path
386
- + ", readTimeSeconds="
387
- + readTimeSeconds
388
- + ", readTimeNanos="
389
- + readTimeNanos
390
- + ", documentType="
391
- + documentType
392
- + ")" ;
440
+ private static class BackfillKey {
441
+ final String path ;
442
+ final int readTimeSeconds ;
443
+ final int readTimeNanos ;
444
+
445
+ BackfillKey (String path , int readTimeSeconds , int readTimeNanos ) {
446
+ this .path = path ;
447
+ this .readTimeSeconds = readTimeSeconds ;
448
+ this .readTimeNanos = readTimeNanos ;
449
+ }
450
+
451
+ @ NonNull
452
+ @ Override
453
+ public String toString () {
454
+ return "DocumentTypeBackfills.BackfillKey(path="
455
+ + path
456
+ + ", readTimeSeconds="
457
+ + readTimeSeconds
458
+ + ", readTimeNanos="
459
+ + readTimeNanos
460
+ + ")" ;
461
+ }
393
462
}
394
463
}
395
464
}
0 commit comments