19
19
import static com .google .firebase .firestore .util .Assert .hardAssert ;
20
20
21
21
import android .database .sqlite .SQLiteStatement ;
22
+ import android .os .ParcelFileDescriptor ;
22
23
import com .google .firebase .Timestamp ;
23
24
import com .google .firebase .firestore .auth .User ;
24
25
import com .google .firebase .firestore .core .Query ;
31
32
import com .google .protobuf .ByteString ;
32
33
import com .google .protobuf .InvalidProtocolBufferException ;
33
34
import com .google .protobuf .MessageLite ;
35
+ import java .io .IOException ;
34
36
import java .util .ArrayList ;
35
37
import java .util .Arrays ;
36
38
import java .util .Collections ;
42
44
/** A mutation queue for a specific user, backed by SQLite. */
43
45
final class SQLiteMutationQueue implements MutationQueue {
44
46
47
+ private static final int BLOB_MAX_INLINE_LENGTH = 1000000 ;
48
+
45
49
private final SQLitePersistence db ;
46
50
private final LocalSerializer serializer ;
47
51
@@ -204,9 +208,12 @@ public MutationBatch addMutationBatch(Timestamp localWriteTime, List<Mutation> m
204
208
@ Nullable
205
209
@ Override
206
210
public MutationBatch lookupMutationBatch (int batchId ) {
207
- return db .query ("SELECT mutations FROM mutations WHERE uid = ? AND batch_id = ?" )
208
- .binding (uid , batchId )
209
- .firstValue (row -> decodeMutationBatch (row .getBlob (0 )));
211
+ return db .query (
212
+ "SELECT m.batch_id, SUBSTR(m.mutations, 1, ?) "
213
+ + "FROM mutations m "
214
+ + "WHERE uid = ? AND batch_id = ?" )
215
+ .binding (BLOB_MAX_INLINE_LENGTH , uid , batchId )
216
+ .firstValue (row -> decodeMutationBatchRow (row .getInt (0 ), row .getBlob (1 )));
210
217
}
211
218
212
219
@ Nullable
@@ -215,19 +222,23 @@ public MutationBatch getNextMutationBatchAfterBatchId(int batchId) {
215
222
int nextBatchId = batchId + 1 ;
216
223
217
224
return db .query (
218
- "SELECT mutations FROM mutations "
225
+ "SELECT m.batch_id, SUBSTR(m.mutations, 1, ?) "
226
+ + "FROM mutations m "
219
227
+ "WHERE uid = ? AND batch_id >= ? "
220
228
+ "ORDER BY batch_id ASC LIMIT 1" )
221
- .binding (uid , nextBatchId )
222
- .firstValue (row -> decodeMutationBatch (row .getBlob ( 0 )));
229
+ .binding (BLOB_MAX_INLINE_LENGTH , uid , nextBatchId )
230
+ .firstValue (row -> decodeMutationBatchRow (row .getInt ( 0 ), row . getBlob ( 1 )));
223
231
}
224
232
225
233
@ Override
226
234
public List <MutationBatch > getAllMutationBatches () {
227
235
List <MutationBatch > result = new ArrayList <>();
228
- db .query ("SELECT mutations FROM mutations WHERE uid = ? ORDER BY batch_id ASC" )
229
- .binding (uid )
230
- .forEach (row -> result .add (decodeMutationBatch (row .getBlob (0 ))));
236
+ db .query (
237
+ "SELECT m.batch_id, SUBSTR(m.mutations, 1, ?) "
238
+ + "FROM mutations m "
239
+ + "WHERE uid = ? ORDER BY batch_id ASC" )
240
+ .binding (BLOB_MAX_INLINE_LENGTH , uid )
241
+ .forEach (row -> result .add (decodeMutationBatchRow (row .getInt (0 ), row .getBlob (1 ))));
231
242
return result ;
232
243
}
233
244
@@ -237,14 +248,15 @@ public List<MutationBatch> getAllMutationBatchesAffectingDocumentKey(DocumentKey
237
248
238
249
List <MutationBatch > result = new ArrayList <>();
239
250
db .query (
240
- "SELECT m.mutations FROM document_mutations dm, mutations m "
251
+ "SELECT m.batch_id, SUBSTR(m.mutations, 1, ?) "
252
+ + "FROM document_mutations dm, mutations m "
241
253
+ "WHERE dm.uid = ? "
242
254
+ "AND dm.path = ? "
243
255
+ "AND dm.uid = m.uid "
244
256
+ "AND dm.batch_id = m.batch_id "
245
257
+ "ORDER BY dm.batch_id" )
246
- .binding (uid , path )
247
- .forEach (row -> result .add (decodeMutationBatch (row .getBlob ( 0 ))));
258
+ .binding (BLOB_MAX_INLINE_LENGTH , uid , path )
259
+ .forEach (row -> result .add (decodeMutationBatchRow (row .getInt ( 0 ), row . getBlob ( 1 ))));
248
260
return result ;
249
261
}
250
262
@@ -259,10 +271,11 @@ public List<MutationBatch> getAllMutationBatchesAffectingDocumentKeys(
259
271
SQLitePersistence .LongQuery longQuery =
260
272
new SQLitePersistence .LongQuery (
261
273
db ,
262
- "SELECT DISTINCT dm.batch_id, m.mutations FROM document_mutations dm, mutations m "
274
+ "SELECT DISTINCT dm.batch_id, SUBSTR(m.mutations, 1, ?) "
275
+ + "FROM document_mutations dm, mutations m "
263
276
+ "WHERE dm.uid = ? "
264
277
+ "AND dm.path IN (" ,
265
- Arrays .asList (uid ),
278
+ Arrays .asList (BLOB_MAX_INLINE_LENGTH , uid ),
266
279
args ,
267
280
") "
268
281
+ "AND dm.uid = m.uid "
@@ -279,7 +292,7 @@ public List<MutationBatch> getAllMutationBatchesAffectingDocumentKeys(
279
292
int batchId = row .getInt (0 );
280
293
if (!uniqueBatchIds .contains (batchId )) {
281
294
uniqueBatchIds .add (batchId );
282
- result .add (decodeMutationBatch ( row .getBlob (1 )));
295
+ result .add (decodeMutationBatchRow ( batchId , row .getBlob (1 )));
283
296
}
284
297
});
285
298
}
@@ -321,14 +334,15 @@ public List<MutationBatch> getAllMutationBatchesAffectingQuery(Query query) {
321
334
322
335
List <MutationBatch > result = new ArrayList <>();
323
336
db .query (
324
- "SELECT dm.batch_id, dm.path, m.mutations FROM document_mutations dm, mutations m "
337
+ "SELECT dm.batch_id, dm.path, SUBSTR(m.mutations, 1, ?) "
338
+ + "FROM document_mutations dm, mutations m "
325
339
+ "WHERE dm.uid = ? "
326
340
+ "AND dm.path >= ? "
327
341
+ "AND dm.path < ? "
328
342
+ "AND dm.uid = m.uid "
329
343
+ "AND dm.batch_id = m.batch_id "
330
344
+ "ORDER BY dm.batch_id" )
331
- .binding (uid , prefixPath , prefixSuccessorPath )
345
+ .binding (BLOB_MAX_INLINE_LENGTH , uid , prefixPath , prefixSuccessorPath )
332
346
.forEach (
333
347
row -> {
334
348
// Ensure unique batches only. This works because the batches come out in order so we
@@ -350,7 +364,7 @@ public List<MutationBatch> getAllMutationBatchesAffectingQuery(Query query) {
350
364
return ;
351
365
}
352
366
353
- result .add (decodeMutationBatch ( row .getBlob (2 )));
367
+ result .add (decodeMutationBatchRow ( batchId , row .getBlob (2 )));
354
368
});
355
369
356
370
return result ;
@@ -399,6 +413,38 @@ public void performConsistencyCheck() {
399
413
danglingMutationReferences );
400
414
}
401
415
416
+ /**
417
+ * Decodes a mutation batch row containing a batch id and a substring of a blob. If the blob is
418
+ * too large, executes another query to load the blob directly.
419
+ *
420
+ * @param batchId The batch ID of the row containing the blob
421
+ * @param bytes The bytes represented
422
+ * @return
423
+ */
424
+ private MutationBatch decodeMutationBatchRow (int batchId , byte [] bytes ) {
425
+ if (bytes .length < BLOB_MAX_INLINE_LENGTH ) {
426
+ return decodeMutationBatch (bytes );
427
+ }
428
+
429
+ SQLiteStatement loader =
430
+ db .prepare ("SELECT mutations FROM mutations WHERE uid = ? AND batch_id = ?" );
431
+ loader .bindString (1 , uid );
432
+ loader .bindLong (2 , batchId );
433
+
434
+ ParcelFileDescriptor blobFile = loader .simpleQueryForBlobFileDescriptor ();
435
+ hardAssert (blobFile != null , "Blob exists so descriptor should not be null" );
436
+
437
+ try (ParcelFileDescriptor .AutoCloseInputStream stream =
438
+ new ParcelFileDescriptor .AutoCloseInputStream (blobFile )) {
439
+ return serializer .decodeMutationBatch (
440
+ com .google .firebase .firestore .proto .WriteBatch .parseFrom (stream ));
441
+ } catch (InvalidProtocolBufferException e ) {
442
+ throw fail ("MutationBatch failed to parse: %s" , e );
443
+ } catch (IOException e ) {
444
+ throw fail ("Failed to read blob for uid=%s, batch_id=%d: %s" , uid , batchId , e );
445
+ }
446
+ }
447
+
402
448
private MutationBatch decodeMutationBatch (byte [] bytes ) {
403
449
try {
404
450
return serializer .decodeMutationBatch (
0 commit comments