Skip to content

Commit ff714a2

Browse files
Firestore: Split schema migrations into batches (#374)
1 parent 2abc69b commit ff714a2

File tree

3 files changed

+75
-13
lines changed

3 files changed

+75
-13
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
# Unreleased
2+
- [fixed] Fixed an issue that prevented schema migrations for clients with
3+
large offline datasets (#370).
4+
5+
# To be released - 19.0.0
26
- [feature] You can now query across all collections in your database with a
37
given collection ID using the `FirebaseFirestore.collectionGroup()` method.
48
- [changed] The garbage collection process for on-disk persistence that

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteSchema.java

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ class SQLiteSchema {
5050
// Remove this constant and increment VERSION to enable indexing support
5151
static final int INDEXING_SUPPORT_VERSION = VERSION + 1;
5252

53+
/**
54+
* The batch size for the sequence number migration in `ensureSequenceNumbers()`.
55+
*
56+
* <p>This addresses https://github.com/firebase/firebase-android-sdk/issues/370, where a customer
57+
* reported that schema migrations failed for clients with thousands of documents. The number has
58+
* been chosen based on manual experiments.
59+
*/
60+
private static final int SEQUENCE_NUMBER_BATCH_SIZE = 100;
61+
5362
private final SQLiteDatabase db;
5463

5564
// PORTING NOTE: The Android client doesn't need to use a serializer to remove held write acks.
@@ -359,17 +368,30 @@ private void ensureSequenceNumbers() {
359368
SQLiteStatement tagDocument =
360369
db.compileStatement(
361370
"INSERT INTO target_documents (target_id, path, sequence_number) VALUES (0, ?, ?)");
371+
362372
SQLitePersistence.Query untaggedDocumentsQuery =
363373
new SQLitePersistence.Query(
364-
db,
365-
"SELECT RD.path FROM remote_documents AS RD WHERE NOT EXISTS (SELECT TD.path FROM target_documents AS TD WHERE RD.path = TD.path AND TD.target_id = 0)");
366-
untaggedDocumentsQuery.forEach(
367-
row -> {
368-
tagDocument.clearBindings();
369-
tagDocument.bindString(1, row.getString(0));
370-
tagDocument.bindLong(2, sequenceNumber);
371-
hardAssert(tagDocument.executeInsert() != -1, "Failed to insert a sentinel row");
372-
});
374+
db,
375+
"SELECT RD.path FROM remote_documents AS RD WHERE NOT EXISTS ("
376+
+ "SELECT TD.path FROM target_documents AS TD "
377+
+ "WHERE RD.path = TD.path AND TD.target_id = 0"
378+
+ ") LIMIT ?")
379+
.binding(SEQUENCE_NUMBER_BATCH_SIZE);
380+
381+
boolean[] resultsRemaining = new boolean[1];
382+
383+
do {
384+
resultsRemaining[0] = false;
385+
386+
untaggedDocumentsQuery.forEach(
387+
row -> {
388+
resultsRemaining[0] = true;
389+
tagDocument.clearBindings();
390+
tagDocument.bindString(1, row.getString(0));
391+
tagDocument.bindLong(2, sequenceNumber);
392+
hardAssert(tagDocument.executeInsert() != -1, "Failed to insert a sentinel row");
393+
});
394+
} while (resultsRemaining[0]);
373395
}
374396

375397
private void createV8CollectionParentsIndex() {

firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteSchemaTest.java

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,12 +279,12 @@ public void addsSentinelRows() {
279279
// Set up some documents (we only need the keys)
280280
// For the odd ones, add sentinel rows.
281281
for (int i = 0; i < 10; i++) {
282-
String path = "docs/doc_" + i;
283-
db.execSQL("INSERT INTO remote_documents (path) VALUES (?)", new String[] {path});
282+
String encodedPath = encode(path("docs/doc_" + i));
283+
db.execSQL("INSERT INTO remote_documents (path) VALUES (?)", new String[] {encodedPath});
284284
if (i % 2 == 1) {
285285
db.execSQL(
286286
"INSERT INTO target_documents (target_id, path, sequence_number) VALUES (0, ?, ?)",
287-
new Object[] {path, oldSequenceNumber});
287+
new Object[] {encodedPath, oldSequenceNumber});
288288
}
289289
}
290290

@@ -295,7 +295,7 @@ public void addsSentinelRows() {
295295
db, "SELECT path, sequence_number FROM target_documents WHERE target_id = 0")
296296
.forEach(
297297
row -> {
298-
String path = row.getString(0);
298+
String path = decodeResourcePath(row.getString(0)).getLastSegment();
299299
long sequenceNumber = row.getLong(1);
300300

301301
int docNum = Integer.parseInt(path.split("_", -1)[1]);
@@ -307,6 +307,42 @@ public void addsSentinelRows() {
307307
});
308308
}
309309

310+
@Test
311+
public void addsSentinelRowsForLargeNumberOfDocuments() {
312+
// This test only verifies that we add sentinal rows for documents even if the number of
313+
// documents exceed our batch size. It does not verify that we do not hit a
314+
// `SQLiteBlobTooBigException`, since this exception is not thrown when using SQLite through
315+
// Robolectric.
316+
317+
// PORTING NOTE: This test only exists on Android since other clients do not need to split
318+
// large data sets during schema migration.
319+
320+
schema.runMigrations(0, 6);
321+
322+
// Set up some documents (we only need the keys). Note this count is higher than the batch size
323+
// during migration, which is 100.
324+
int documentCount = 250;
325+
for (int i = 0; i < documentCount; i++) {
326+
String path = "coll/doc_" + i;
327+
db.execSQL(
328+
"INSERT INTO remote_documents (path) VALUES (?)", new String[] {encode(path(path))});
329+
}
330+
331+
new SQLitePersistence.Query(db, "SELECT COUNT(*) FROM target_documents")
332+
.first(
333+
row -> {
334+
assertEquals(0, row.getLong(0));
335+
});
336+
337+
schema.runMigrations(6, 7);
338+
339+
new SQLitePersistence.Query(db, "SELECT COUNT(*) FROM target_documents")
340+
.first(
341+
row -> {
342+
assertEquals(documentCount, row.getLong(0));
343+
});
344+
}
345+
310346
@Test
311347
public void canCreateCollectionParentsIndex() {
312348
// This test creates a database with schema version 7 that has a few

0 commit comments

Comments
 (0)